refactor(chunk): restructured language classifiers to declarative rule tables
- Migrated language classifiers from imperative methods to declarative semantic rule tables with ClassifierTables and StructuralOverrides. - Extracted node shape analysis into dedicated shape module with field priority constants and helper functions for AST traversal. - Added schema module with language-aware node metadata and thread-local language context management. - Centralized environment variable parsing across codebase using $flag(), $envpos(), and isBunTestRuntime() utilities. - Added PI_CHUNK_AUTOINDENT configuration to control indentation normalization in chunk read/edit operations. - Enhanced system prompt with instruction priority, output contract, tool persistence, and completeness guidelines.
This commit is contained in:
@@ -144,3 +144,5 @@ windows-sys = { version = "0.61", features = [
|
||||
[build-dependencies]
|
||||
napi-build = "2"
|
||||
cc = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
+635
-1
@@ -1,7 +1,357 @@
|
||||
use std::path::Path;
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
const IDENTIFIER_FIELD_PRIORITY: &[&str] = &[
|
||||
"name",
|
||||
"identifier",
|
||||
"attrpath",
|
||||
"key",
|
||||
"label",
|
||||
"alias",
|
||||
"field",
|
||||
"member",
|
||||
"property",
|
||||
"tag",
|
||||
"target",
|
||||
"variable",
|
||||
];
|
||||
|
||||
const BODY_FIELD_PRIORITY: &[&str] = &["body", "value", "declaration_list", "block", "members"];
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct GrammarSpec {
|
||||
language: &'static str,
|
||||
package: &'static str,
|
||||
node_types_rel: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RawTypeRef {
|
||||
#[serde(rename = "type")]
|
||||
kind: Option<String>,
|
||||
named: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RawFieldSpec {
|
||||
#[serde(default)]
|
||||
types: Vec<RawTypeRef>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RawNodeType {
|
||||
#[serde(rename = "type")]
|
||||
kind: Option<String>,
|
||||
fields: Option<BTreeMap<String, RawFieldSpec>>,
|
||||
children: Option<RawFieldSpec>,
|
||||
subtypes: Option<Vec<RawTypeRef>>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct GeneratedSchema {
|
||||
languages: BTreeMap<String, BTreeMap<String, GeneratedNodeTypeSchema>>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct GeneratedNodeTypeSchema {
|
||||
identifier_fields: Vec<String>,
|
||||
body_fields: Vec<String>,
|
||||
container_child_kinds: Vec<String>,
|
||||
is_supertype: bool,
|
||||
has_structural_children: bool,
|
||||
}
|
||||
|
||||
const GRAMMARS: &[GrammarSpec] = &[
|
||||
GrammarSpec {
|
||||
language: "astro",
|
||||
package: "tree-sitter-astro-next",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "bash",
|
||||
package: "tree-sitter-bash",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "c",
|
||||
package: "tree-sitter-c",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "clojure",
|
||||
package: "tree-sitter-clojure",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "cmake",
|
||||
package: "tree-sitter-cmake",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "cpp",
|
||||
package: "tree-sitter-cpp",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "csharp",
|
||||
package: "tree-sitter-c-sharp",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "css",
|
||||
package: "tree-sitter-css",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "diff",
|
||||
package: "tree-sitter-diff",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "dockerfile",
|
||||
package: "tree-sitter-dockerfile-updated",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "elixir",
|
||||
package: "tree-sitter-elixir",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "erlang",
|
||||
package: "tree-sitter-erlang",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "go",
|
||||
package: "tree-sitter-go",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "graphql",
|
||||
package: "tree-sitter-graphql",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "handlebars",
|
||||
package: "tree-sitter-glimmer",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "haskell",
|
||||
package: "tree-sitter-haskell",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "hcl",
|
||||
package: "tree-sitter-hcl",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "html",
|
||||
package: "tree-sitter-html",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "ini",
|
||||
package: "tree-sitter-ini",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "java",
|
||||
package: "tree-sitter-java",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "javascript",
|
||||
package: "tree-sitter-javascript",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "json",
|
||||
package: "tree-sitter-json",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "toml",
|
||||
package: "tree-sitter-toml-ng",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "just",
|
||||
package: "tree-sitter-just",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "julia",
|
||||
package: "tree-sitter-julia",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "kotlin",
|
||||
package: "tree-sitter-kotlin-sg",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "lua",
|
||||
package: "tree-sitter-lua",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "make",
|
||||
package: "tree-sitter-make",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "markdown",
|
||||
package: "tree-sitter-md",
|
||||
node_types_rel: "tree-sitter-markdown/src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "nix",
|
||||
package: "tree-sitter-nix",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "objc",
|
||||
package: "tree-sitter-objc",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "odin",
|
||||
package: "tree-sitter-odin",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "perl",
|
||||
package: "tree-sitter-perl-next",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "php",
|
||||
package: "tree-sitter-php",
|
||||
node_types_rel: "php/src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "powershell",
|
||||
package: "tree-sitter-powershell",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "protobuf",
|
||||
package: "tree-sitter-proto",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "python",
|
||||
package: "tree-sitter-python",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "r",
|
||||
package: "tree-sitter-r",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "regex",
|
||||
package: "tree-sitter-regex",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "ruby",
|
||||
package: "tree-sitter-ruby",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "rust",
|
||||
package: "tree-sitter-rust",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "scala",
|
||||
package: "tree-sitter-scala",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "solidity",
|
||||
package: "tree-sitter-solidity",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "sql",
|
||||
package: "tree-sitter-sequel",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "starlark",
|
||||
package: "tree-sitter-starlark",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "svelte",
|
||||
package: "tree-sitter-svelte-next",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "swift",
|
||||
package: "tree-sitter-swift",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "toml",
|
||||
package: "tree-sitter-toml-ng",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "tlaplus",
|
||||
package: "tree-sitter-tlaplus",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "tsx",
|
||||
package: "tree-sitter-typescript",
|
||||
node_types_rel: "tsx/src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "typescript",
|
||||
package: "tree-sitter-typescript",
|
||||
node_types_rel: "typescript/src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "verilog",
|
||||
package: "tree-sitter-verilog",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "vue",
|
||||
package: "tree-sitter-vue-next",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "xml",
|
||||
package: "tree-sitter-xml",
|
||||
node_types_rel: "xml/src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "yaml",
|
||||
package: "tree-sitter-yaml",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
GrammarSpec {
|
||||
language: "zig",
|
||||
package: "tree-sitter-zig",
|
||||
node_types_rel: "src/node-types.json",
|
||||
},
|
||||
];
|
||||
|
||||
fn main() {
|
||||
napi_build::setup();
|
||||
generate_chunk_schema();
|
||||
|
||||
let scanner_dir = Path::new("vendor/tree-sitter-glimmer");
|
||||
let scanner_path = scanner_dir.join("scanner.c");
|
||||
@@ -24,3 +374,287 @@ fn main() {
|
||||
|
||||
build.compile("tree-sitter-glimmer-scanner");
|
||||
}
|
||||
|
||||
fn generate_chunk_schema() {
|
||||
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR should be set");
|
||||
let workspace_root = Path::new(&manifest_dir)
|
||||
.parent()
|
||||
.and_then(Path::parent)
|
||||
.expect("pi-natives should live under the workspace root");
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR should be set"));
|
||||
let output_path = out_dir.join("chunk_schema.json");
|
||||
let locked_versions = locked_package_versions(&workspace_root.join("Cargo.lock"));
|
||||
let registry_roots = cargo_registry_roots();
|
||||
let mut languages = BTreeMap::new();
|
||||
|
||||
for grammar in GRAMMARS {
|
||||
let Some(version) = locked_versions.get(grammar.package) else {
|
||||
continue;
|
||||
};
|
||||
let Some(package_dir) = find_registry_package_dir(®istry_roots, grammar.package, version)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let node_types_path = package_dir.join(grammar.node_types_rel);
|
||||
if !node_types_path.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
println!("cargo:rerun-if-changed={}", node_types_path.display());
|
||||
let source =
|
||||
fs::read_to_string(&node_types_path).expect("node-types.json should be readable");
|
||||
let raw_nodes: Vec<RawNodeType> =
|
||||
serde_json::from_str(&source).expect("node-types.json should parse");
|
||||
let schemas = build_language_schema(raw_nodes);
|
||||
if !schemas.is_empty() {
|
||||
languages.insert(grammar.language.to_string(), schemas);
|
||||
}
|
||||
}
|
||||
|
||||
let generated = GeneratedSchema { languages };
|
||||
let json = serde_json::to_string(&generated).expect("schema JSON should serialize");
|
||||
fs::write(output_path, json).expect("schema JSON should write");
|
||||
}
|
||||
|
||||
fn build_language_schema(raw_nodes: Vec<RawNodeType>) -> BTreeMap<String, GeneratedNodeTypeSchema> {
|
||||
let mut raw_by_kind = HashMap::new();
|
||||
for raw in raw_nodes {
|
||||
let Some(kind) = raw.kind.clone() else {
|
||||
continue;
|
||||
};
|
||||
raw_by_kind.insert(kind, raw);
|
||||
}
|
||||
|
||||
let structural_state = compute_structural_state(&raw_by_kind);
|
||||
let mut out = BTreeMap::new();
|
||||
for (kind, raw) in &raw_by_kind {
|
||||
let identifier_fields = pick_priority_fields(raw.fields.as_ref(), IDENTIFIER_FIELD_PRIORITY);
|
||||
let body_fields = pick_priority_fields(raw.fields.as_ref(), BODY_FIELD_PRIORITY);
|
||||
let container_child_kinds = collect_child_container_kinds(raw, &structural_state);
|
||||
let is_supertype = is_supertype(raw);
|
||||
let has_structural_children = structural_state
|
||||
.get(kind)
|
||||
.is_some_and(|state| state.has_structural_children);
|
||||
|
||||
if identifier_fields.is_empty()
|
||||
&& body_fields.is_empty()
|
||||
&& container_child_kinds.is_empty()
|
||||
&& !is_supertype
|
||||
&& !has_structural_children
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
out.insert(kind.clone(), GeneratedNodeTypeSchema {
|
||||
identifier_fields,
|
||||
body_fields,
|
||||
container_child_kinds,
|
||||
is_supertype,
|
||||
has_structural_children,
|
||||
});
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn pick_priority_fields(
|
||||
fields: Option<&BTreeMap<String, RawFieldSpec>>,
|
||||
priority: &[&str],
|
||||
) -> Vec<String> {
|
||||
let Some(fields) = fields else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
priority
|
||||
.iter()
|
||||
.filter(|field| fields.contains_key(**field))
|
||||
.map(|field| (*field).to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn collect_child_container_kinds(
|
||||
raw: &RawNodeType,
|
||||
structural_state: &HashMap<String, StructuralState>,
|
||||
) -> Vec<String> {
|
||||
let mut kinds = BTreeSet::new();
|
||||
let child_types = raw
|
||||
.children
|
||||
.as_ref()
|
||||
.map(|children| children.types.as_slice())
|
||||
.unwrap_or_default();
|
||||
|
||||
for child in child_types {
|
||||
if !child.named {
|
||||
continue;
|
||||
}
|
||||
let Some(kind) = child.kind.as_deref() else {
|
||||
continue;
|
||||
};
|
||||
if structural_state
|
||||
.get(kind)
|
||||
.copied()
|
||||
.is_some_and(StructuralState::is_structural)
|
||||
{
|
||||
kinds.insert(kind.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
kinds.into_iter().collect()
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct StructuralState {
|
||||
is_structural: bool,
|
||||
has_structural_children: bool,
|
||||
}
|
||||
|
||||
impl StructuralState {
|
||||
const fn is_structural(self) -> bool {
|
||||
self.is_structural
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_structural_state(
|
||||
raw_by_kind: &HashMap<String, RawNodeType>,
|
||||
) -> HashMap<String, StructuralState> {
|
||||
let mut state = raw_by_kind
|
||||
.iter()
|
||||
.map(|(kind, raw)| {
|
||||
let base_structural = is_supertype(raw)
|
||||
|| raw.fields.as_ref().is_some_and(|fields| !fields.is_empty())
|
||||
|| !named_child_type_kinds(raw).is_empty();
|
||||
(kind.clone(), StructuralState {
|
||||
is_structural: base_structural,
|
||||
has_structural_children: false,
|
||||
})
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
loop {
|
||||
let mut changed = false;
|
||||
for (kind, raw) in raw_by_kind {
|
||||
let next_has_structural_children =
|
||||
named_child_type_kinds(raw).into_iter().any(|child_kind| {
|
||||
state
|
||||
.get(child_kind.as_str())
|
||||
.copied()
|
||||
.is_some_and(StructuralState::is_structural)
|
||||
});
|
||||
|
||||
let entry = state
|
||||
.get_mut(kind.as_str())
|
||||
.expect("every raw node should have structural state");
|
||||
let next_is_structural = entry.is_structural || next_has_structural_children;
|
||||
if next_is_structural != entry.is_structural
|
||||
|| next_has_structural_children != entry.has_structural_children
|
||||
{
|
||||
entry.is_structural = next_is_structural;
|
||||
entry.has_structural_children = next_has_structural_children;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
|
||||
fn is_supertype(raw: &RawNodeType) -> bool {
|
||||
raw.subtypes
|
||||
.as_ref()
|
||||
.is_some_and(|subtypes| !subtypes.is_empty())
|
||||
}
|
||||
|
||||
fn named_child_type_kinds(raw: &RawNodeType) -> BTreeSet<String> {
|
||||
let mut kinds = BTreeSet::new();
|
||||
if let Some(fields) = raw.fields.as_ref() {
|
||||
for field in fields.values() {
|
||||
for field_type in &field.types {
|
||||
if field_type.named
|
||||
&& let Some(kind) = &field_type.kind
|
||||
{
|
||||
kinds.insert(kind.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(children) = raw.children.as_ref() {
|
||||
for child in &children.types {
|
||||
if child.named
|
||||
&& let Some(kind) = &child.kind
|
||||
{
|
||||
kinds.insert(kind.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kinds
|
||||
}
|
||||
|
||||
fn cargo_registry_roots() -> Vec<PathBuf> {
|
||||
let mut roots = Vec::new();
|
||||
if let Some(cargo_home) = env::var_os("CARGO_HOME") {
|
||||
roots.push(PathBuf::from(cargo_home).join("registry").join("src"));
|
||||
}
|
||||
if let Some(home) = env::var_os("HOME") {
|
||||
roots.push(
|
||||
PathBuf::from(home)
|
||||
.join(".cargo")
|
||||
.join("registry")
|
||||
.join("src"),
|
||||
);
|
||||
}
|
||||
roots
|
||||
}
|
||||
|
||||
fn find_registry_package_dir(
|
||||
registry_roots: &[PathBuf],
|
||||
package: &str,
|
||||
version: &str,
|
||||
) -> Option<PathBuf> {
|
||||
for registry_root in registry_roots {
|
||||
let Ok(registry_dirs) = fs::read_dir(registry_root) else {
|
||||
continue;
|
||||
};
|
||||
for registry_dir in registry_dirs.flatten() {
|
||||
let candidate = registry_dir.path().join(format!("{package}-{version}"));
|
||||
if candidate.exists() {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn locked_package_versions(lock_path: &Path) -> HashMap<String, String> {
|
||||
let source = fs::read_to_string(lock_path).expect("Cargo.lock should be readable");
|
||||
let mut versions = HashMap::new();
|
||||
let mut current_name = None;
|
||||
let mut current_version = None;
|
||||
|
||||
for line in source.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed == "[[package]]" {
|
||||
if let (Some(name), Some(version)) = (current_name.take(), current_version.take()) {
|
||||
versions.insert(name, version);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if let Some(value) = trimmed.strip_prefix("name = \"") {
|
||||
current_name = value.strip_suffix('"').map(ToOwned::to_owned);
|
||||
continue;
|
||||
}
|
||||
if let Some(value) = trimmed.strip_prefix("version = \"") {
|
||||
current_version = value.strip_suffix('"').map(ToOwned::to_owned);
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(name), Some(version)) = (current_name, current_version) {
|
||||
versions.insert(name, version);
|
||||
}
|
||||
|
||||
versions
|
||||
}
|
||||
|
||||
@@ -2,26 +2,39 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{ClassifierTables, LangClassifier, StructuralOverrides},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct AstroClassifier;
|
||||
|
||||
impl LangClassifier for AstroClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_astro_node(node, source)
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
static TABLES: ClassifierTables = ClassifierTables {
|
||||
root: &[],
|
||||
class: &[],
|
||||
function: &[],
|
||||
structural_overrides: StructuralOverrides {
|
||||
extra_trivia: &[],
|
||||
preserved_trivia: &[],
|
||||
extra_root_wrappers: &["document"],
|
||||
preserved_root_wrappers: &[],
|
||||
absorbable_attrs: &[],
|
||||
},
|
||||
};
|
||||
&TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
_context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_astro_node(node, source)
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_astro_node(node, source)
|
||||
}
|
||||
|
||||
fn is_root_wrapper(&self, kind: &str) -> bool {
|
||||
kind == "document"
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_astro_node<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
|
||||
@@ -2,7 +2,14 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{
|
||||
ClassifierTables, LangClassifier, NamingMode, RecurseMode, RuleStyle, StructuralOverrides,
|
||||
semantic_rule,
|
||||
},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct ShellBuildClassifier;
|
||||
|
||||
@@ -72,79 +79,150 @@ impl ShellBuildClassifier {
|
||||
}
|
||||
|
||||
impl LangClassifier for ShellBuildClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
"rule" => {
|
||||
let name =
|
||||
Self::extract_rule_target(node, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Rule,
|
||||
Some(name),
|
||||
source,
|
||||
recurse_into(node, ChunkContext::ClassBody, &[], &["recipe"]),
|
||||
))
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
static TABLES: ClassifierTables = ClassifierTables {
|
||||
root: &[
|
||||
semantic_rule(
|
||||
"conditional",
|
||||
ChunkKind::If,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"command",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"pipeline",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"if_statement",
|
||||
ChunkKind::If,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"case_statement",
|
||||
ChunkKind::Switch,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"while_statement",
|
||||
ChunkKind::Loop,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"for_statement",
|
||||
ChunkKind::Loop,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"function_definition",
|
||||
ChunkKind::Function,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"hunks",
|
||||
ChunkKind::Hunks,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
],
|
||||
class: &[semantic_rule(
|
||||
"hunk",
|
||||
ChunkKind::Hunk,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
)],
|
||||
function: &[
|
||||
semantic_rule(
|
||||
"if_statement",
|
||||
ChunkKind::If,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"case_statement",
|
||||
ChunkKind::Switch,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"while_statement",
|
||||
ChunkKind::Loop,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"for_statement",
|
||||
ChunkKind::Loop,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"command",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"pipeline",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"subshell",
|
||||
ChunkKind::Block,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
],
|
||||
structural_overrides: StructuralOverrides {
|
||||
extra_trivia: &[],
|
||||
preserved_trivia: &[],
|
||||
extra_root_wrappers: &["makefile"],
|
||||
preserved_root_wrappers: &[],
|
||||
absorbable_attrs: &[],
|
||||
},
|
||||
"variable_assignment" | "shell_assignment" => {
|
||||
let name =
|
||||
Self::extract_var_name(node, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
Some(make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None))
|
||||
},
|
||||
"define_directive" => {
|
||||
let name =
|
||||
Self::extract_var_name(node, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
Some(make_kind_chunk(node, ChunkKind::Define, Some(name), source, None))
|
||||
},
|
||||
"conditional" => Some(positional_candidate(node, ChunkKind::If, source)),
|
||||
// Bash commands and pipelines
|
||||
"command" | "pipeline" => Some(group_candidate(node, ChunkKind::Statements, source)),
|
||||
// Bash control flow
|
||||
"if_statement" => Some(positional_candidate(node, ChunkKind::If, source)),
|
||||
"case_statement" => Some(positional_candidate(node, ChunkKind::Switch, source)),
|
||||
"while_statement" | "for_statement" => {
|
||||
Some(positional_candidate(node, ChunkKind::Loop, source))
|
||||
},
|
||||
// Bash function definition
|
||||
"function_definition" => Some(named_candidate(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)),
|
||||
// Diff: top-level file block (one per file in git diff output)
|
||||
"block" => {
|
||||
let identifier = Self::extract_diff_filename(node, source);
|
||||
let recurse = recurse_into(node, ChunkContext::ClassBody, &[], &["hunks"]);
|
||||
let mut candidate =
|
||||
make_container_chunk(node, ChunkKind::File, identifier, source, recurse);
|
||||
// Always expand hunks so individual @@ sections are addressable,
|
||||
// even for small diffs below the leaf threshold.
|
||||
candidate.force_recurse = recurse.is_some();
|
||||
Some(candidate)
|
||||
},
|
||||
// Diff: standalone hunks (plain patches without a diff --git header)
|
||||
"hunks" => Some(group_candidate(node, ChunkKind::Hunks, source)),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
&TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// Individual hunk inside a block's hunks container
|
||||
"hunk" => Some(positional_candidate(node, ChunkKind::Hunk, source)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
"if_statement" => Some(positional_candidate(node, ChunkKind::If, source)),
|
||||
"case_statement" => Some(positional_candidate(node, ChunkKind::Switch, source)),
|
||||
"while_statement" | "for_statement" => {
|
||||
Some(positional_candidate(node, ChunkKind::Loop, source))
|
||||
},
|
||||
"command" | "pipeline" => Some(group_candidate(node, ChunkKind::Statements, source)),
|
||||
"subshell" => Some(positional_candidate(node, ChunkKind::Block, source)),
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match context {
|
||||
ChunkContext::Root => classify_root_custom(node, source),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -157,8 +235,41 @@ impl LangClassifier for ShellBuildClassifier {
|
||||
// Diff file blocks should always preserve hunk children
|
||||
children.iter().any(|c| c.kind == ChunkKind::Hunk)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_root_wrapper(&self, kind: &str) -> bool {
|
||||
kind == "makefile"
|
||||
fn classify_root_custom<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
"rule" => {
|
||||
let name = ShellBuildClassifier::extract_rule_target(node, source)
|
||||
.unwrap_or_else(|| "anonymous".to_string());
|
||||
Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Rule,
|
||||
Some(name),
|
||||
source,
|
||||
recurse_into(node, ChunkContext::ClassBody, &[], &["recipe"]),
|
||||
))
|
||||
},
|
||||
"variable_assignment" | "shell_assignment" => {
|
||||
let name = ShellBuildClassifier::extract_var_name(node, source)
|
||||
.unwrap_or_else(|| "anonymous".to_string());
|
||||
Some(make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None))
|
||||
},
|
||||
"define_directive" => {
|
||||
let name = ShellBuildClassifier::extract_var_name(node, source)
|
||||
.unwrap_or_else(|| "anonymous".to_string());
|
||||
Some(make_kind_chunk(node, ChunkKind::Define, Some(name), source, None))
|
||||
},
|
||||
"block" => {
|
||||
let identifier = ShellBuildClassifier::extract_diff_filename(node, source);
|
||||
let recurse = recurse_into(node, ChunkContext::ClassBody, &[], &["hunks"]);
|
||||
let mut candidate =
|
||||
make_container_chunk(node, ChunkKind::File, identifier, source, recurse);
|
||||
// Always expand hunks so individual @@ sections are addressable,
|
||||
// even for small diffs below the leaf threshold.
|
||||
candidate.force_recurse = recurse.is_some();
|
||||
Some(candidate)
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,18 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, defaults::classify_var_decl, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{
|
||||
ClassifierTables, LangClassifier, NamingMode, RecurseMode, RuleStyle, StructuralOverrides,
|
||||
semantic_rule,
|
||||
},
|
||||
common::*,
|
||||
defaults::classify_var_decl,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct CCppClassifier;
|
||||
|
||||
// ── C/C++ declarator name extraction ────────────────────────────────────
|
||||
|
||||
/// Extract the function name from a C/C++ `function_definition` or
|
||||
/// `function_declaration` node by traversing into the `declarator` chain.
|
||||
fn extract_c_function_name(node: Node<'_>, source: &str) -> Option<String> {
|
||||
@@ -94,217 +100,292 @@ fn extract_c_field_name(node: Node<'_>, source: &str) -> Option<String> {
|
||||
extract_c_declarator_name(decl, source)
|
||||
}
|
||||
|
||||
const C_CPP_ROOT_RULES: &[super::classify::SemanticRule] = &[
|
||||
// ── Imports ──
|
||||
semantic_rule(
|
||||
"include_directive",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"preproc_include",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"using_directive",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"using_statement",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"import_declaration",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"module_import",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
// ── Statements ──
|
||||
semantic_rule(
|
||||
"expression_statement",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
];
|
||||
|
||||
const C_CPP_TABLES: ClassifierTables = ClassifierTables {
|
||||
root: C_CPP_ROOT_RULES,
|
||||
class: &[],
|
||||
function: &[],
|
||||
structural_overrides: StructuralOverrides::EMPTY,
|
||||
};
|
||||
|
||||
impl LangClassifier for CCppClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// ── Imports ──
|
||||
"include_directive" | "preproc_include" | "using_directive" | "using_statement"
|
||||
| "import_declaration" | "module_import" => {
|
||||
Some(group_candidate(node, ChunkKind::Imports, source))
|
||||
},
|
||||
|
||||
// ── Functions ──
|
||||
"function_definition" | "function_declaration" => Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
extract_c_function_name(node, source),
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)),
|
||||
"constructor_definition" => Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Constructor,
|
||||
None,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)),
|
||||
|
||||
// ── Templates (unwrap to find the inner declaration) ──
|
||||
"template_declaration" => {
|
||||
// Find the inner function_definition / class_specifier / etc.
|
||||
let inner = named_children(node).into_iter().find(|c| {
|
||||
matches!(
|
||||
c.kind(),
|
||||
"function_definition"
|
||||
| "function_declaration"
|
||||
| "class_specifier"
|
||||
| "struct_specifier"
|
||||
| "type_alias_declaration"
|
||||
)
|
||||
});
|
||||
match inner {
|
||||
Some(inner) => {
|
||||
let mut candidate = self.classify_root(inner, source)?;
|
||||
// Expand range to include the template<...> prefix
|
||||
candidate.range_start_byte = node.start_byte();
|
||||
candidate.range_start_line = node.start_position().row + 1;
|
||||
candidate.checksum_start_byte = node.start_byte();
|
||||
Some(candidate)
|
||||
},
|
||||
None => Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::Template,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
source,
|
||||
)),
|
||||
}
|
||||
},
|
||||
|
||||
// ── Containers ──
|
||||
"class_specifier" | "class_declaration" | "class_interface" | "class_implementation" => {
|
||||
Some(container_candidate(node, ChunkKind::Class, source, recurse_class(node)))
|
||||
},
|
||||
"struct_specifier" | "struct_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Struct, source, recurse_class(node)))
|
||||
},
|
||||
"enum_specifier" | "enum_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Enum, source, recurse_enum(node)))
|
||||
},
|
||||
"namespace_definition" => {
|
||||
Some(container_candidate(node, ChunkKind::Module, source, recurse_class(node)))
|
||||
},
|
||||
"union_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Union, source, recurse_class(node)))
|
||||
},
|
||||
|
||||
// ── Types ──
|
||||
"type_alias_declaration" | "user_defined_type_definition" => {
|
||||
Some(named_candidate(node, ChunkKind::Type, source, recurse_class(node)))
|
||||
},
|
||||
|
||||
// ── Variables / assignments ──
|
||||
"variable_declaration" => Some(classify_var_decl(node, source)),
|
||||
"assignment_statement" | "property_declaration" => {
|
||||
Some(group_candidate(node, ChunkKind::Declarations, source))
|
||||
},
|
||||
|
||||
// ── Macros ──
|
||||
"macro_definition" => Some(named_candidate(
|
||||
node,
|
||||
ChunkKind::Macro,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)),
|
||||
|
||||
// ── Control flow (top-level scripts) ──
|
||||
"if_statement" | "switch_statement" | "for_statement" | "while_statement"
|
||||
| "do_statement" | "try_block" => Some(classify_function_c(node, source)),
|
||||
|
||||
// ── Statements ──
|
||||
"expression_statement" => Some(group_candidate(node, ChunkKind::Statements, source)),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
&C_CPP_TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// ── Methods ──
|
||||
"function_definition" | "function_declaration" | "method_declaration" => {
|
||||
let name = extract_c_function_name(node, source)
|
||||
.or_else(|| extract_identifier(node, source))
|
||||
.unwrap_or_else(|| "anonymous".to_string());
|
||||
if name == "constructor" {
|
||||
Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Constructor,
|
||||
None,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
))
|
||||
} else {
|
||||
Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
Some(name),
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
))
|
||||
}
|
||||
},
|
||||
|
||||
// ── Constructors ──
|
||||
"constructor_definition" | "constructor_declaration" => Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Constructor,
|
||||
None,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)),
|
||||
|
||||
// ── Fields ──
|
||||
"field_declaration" => Some(match extract_c_field_name(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Field, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Fields, source),
|
||||
}),
|
||||
|
||||
// ── Enum variants ──
|
||||
"enum_constant" => Some(match extract_identifier(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Variant, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Variants, source),
|
||||
}),
|
||||
|
||||
// ── Nested containers ──
|
||||
"class_specifier" | "class_declaration" | "class_interface" | "class_implementation" => {
|
||||
Some(container_candidate(node, ChunkKind::Class, source, recurse_class(node)))
|
||||
},
|
||||
"struct_specifier" | "struct_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Struct, source, recurse_class(node)))
|
||||
},
|
||||
"enum_specifier" | "enum_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Enum, source, recurse_enum(node)))
|
||||
},
|
||||
"union_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Union, source, recurse_class(node)))
|
||||
},
|
||||
"namespace_definition" => {
|
||||
Some(container_candidate(node, ChunkKind::Module, source, recurse_class(node)))
|
||||
},
|
||||
|
||||
// ── Templates (class body) ──
|
||||
"template_declaration" => {
|
||||
let inner = named_children(node).into_iter().find(|c| {
|
||||
matches!(
|
||||
c.kind(),
|
||||
"function_definition"
|
||||
| "function_declaration"
|
||||
| "class_specifier"
|
||||
| "struct_specifier"
|
||||
| "type_alias_declaration"
|
||||
)
|
||||
});
|
||||
match inner {
|
||||
Some(inner) => {
|
||||
let mut candidate = self.classify_class(inner, source)?;
|
||||
candidate.range_start_byte = node.start_byte();
|
||||
candidate.range_start_line = node.start_position().row + 1;
|
||||
candidate.checksum_start_byte = node.start_byte();
|
||||
Some(candidate)
|
||||
},
|
||||
None => Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::Template,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
source,
|
||||
)),
|
||||
}
|
||||
},
|
||||
|
||||
// ── Types ──
|
||||
"type_alias_declaration" => Some(named_candidate(node, ChunkKind::Type, source, None)),
|
||||
|
||||
_ => None,
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match context {
|
||||
ChunkContext::Root => classify_root_custom(self, node, source),
|
||||
ChunkContext::ClassBody => classify_class_custom(self, node, source),
|
||||
ChunkContext::FunctionBody => Some(classify_function_c(node, source)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(classify_function_c(node, source))
|
||||
fn classify_root_custom<'t>(
|
||||
classifier: &CCppClassifier,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// ── Functions ──
|
||||
"function_definition" | "function_declaration" => Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
extract_c_function_name(node, source),
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)),
|
||||
"constructor_definition" => Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Constructor,
|
||||
None,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)),
|
||||
|
||||
// ── Templates (unwrap to find the inner declaration) ──
|
||||
"template_declaration" => {
|
||||
// Find the inner function_definition / class_specifier / etc.
|
||||
let inner = named_children(node).into_iter().find(|c| {
|
||||
matches!(
|
||||
c.kind(),
|
||||
"function_definition"
|
||||
| "function_declaration"
|
||||
| "class_specifier"
|
||||
| "struct_specifier"
|
||||
| "type_alias_declaration"
|
||||
)
|
||||
});
|
||||
match inner {
|
||||
Some(inner) => {
|
||||
let mut candidate =
|
||||
classifier.classify_override(ChunkContext::Root, inner, source)?;
|
||||
// Expand range to include the template<...> prefix
|
||||
candidate.range_start_byte = node.start_byte();
|
||||
candidate.range_start_line = node.start_position().row + 1;
|
||||
candidate.checksum_start_byte = node.start_byte();
|
||||
Some(candidate)
|
||||
},
|
||||
None => Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::Template,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
source,
|
||||
)),
|
||||
}
|
||||
},
|
||||
|
||||
// ── Containers ──
|
||||
"class_specifier" | "class_declaration" | "class_interface" | "class_implementation" => {
|
||||
Some(container_candidate(node, ChunkKind::Class, source, recurse_class(node)))
|
||||
},
|
||||
"struct_specifier" | "struct_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Struct, source, recurse_class(node)))
|
||||
},
|
||||
"enum_specifier" | "enum_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Enum, source, recurse_enum(node)))
|
||||
},
|
||||
"namespace_definition" => {
|
||||
Some(container_candidate(node, ChunkKind::Module, source, recurse_class(node)))
|
||||
},
|
||||
"union_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Union, source, recurse_class(node)))
|
||||
},
|
||||
|
||||
// ── Types ──
|
||||
"type_alias_declaration" | "user_defined_type_definition" => {
|
||||
Some(named_candidate(node, ChunkKind::Type, source, recurse_class(node)))
|
||||
},
|
||||
|
||||
// ── Variables / assignments ──
|
||||
"variable_declaration" => Some(classify_var_decl(node, source)),
|
||||
"assignment_statement" | "property_declaration" => {
|
||||
Some(group_candidate(node, ChunkKind::Declarations, source))
|
||||
},
|
||||
|
||||
// ── Macros ──
|
||||
"macro_definition" => Some(named_candidate(
|
||||
node,
|
||||
ChunkKind::Macro,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)),
|
||||
|
||||
// ── Control flow (top-level scripts) ──
|
||||
"if_statement" | "switch_statement" | "for_statement" | "while_statement"
|
||||
| "do_statement" | "try_block" => Some(classify_function_c(node, source)),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_class_custom<'t>(
|
||||
classifier: &CCppClassifier,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// ── Methods ──
|
||||
"function_definition" | "function_declaration" | "method_declaration" => {
|
||||
let name = extract_c_function_name(node, source)
|
||||
.or_else(|| extract_identifier(node, source))
|
||||
.unwrap_or_else(|| "anonymous".to_string());
|
||||
if name == "constructor" {
|
||||
Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Constructor,
|
||||
None,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
))
|
||||
} else {
|
||||
Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
Some(name),
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
))
|
||||
}
|
||||
},
|
||||
|
||||
// ── Constructors ──
|
||||
"constructor_definition" | "constructor_declaration" => Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Constructor,
|
||||
None,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)),
|
||||
|
||||
// ── Fields ──
|
||||
"field_declaration" => Some(match extract_c_field_name(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Field, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Fields, source),
|
||||
}),
|
||||
|
||||
// ── Enum variants ──
|
||||
"enum_constant" => Some(match extract_identifier(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Variant, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Variants, source),
|
||||
}),
|
||||
|
||||
// ── Nested containers ──
|
||||
"class_specifier" | "class_declaration" | "class_interface" | "class_implementation" => {
|
||||
Some(container_candidate(node, ChunkKind::Class, source, recurse_class(node)))
|
||||
},
|
||||
"struct_specifier" | "struct_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Struct, source, recurse_class(node)))
|
||||
},
|
||||
"enum_specifier" | "enum_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Enum, source, recurse_enum(node)))
|
||||
},
|
||||
"union_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Union, source, recurse_class(node)))
|
||||
},
|
||||
"namespace_definition" => {
|
||||
Some(container_candidate(node, ChunkKind::Module, source, recurse_class(node)))
|
||||
},
|
||||
|
||||
// ── Templates (class body) ──
|
||||
"template_declaration" => {
|
||||
let inner = named_children(node).into_iter().find(|c| {
|
||||
matches!(
|
||||
c.kind(),
|
||||
"function_definition"
|
||||
| "function_declaration"
|
||||
| "class_specifier"
|
||||
| "struct_specifier"
|
||||
| "type_alias_declaration"
|
||||
)
|
||||
});
|
||||
match inner {
|
||||
Some(inner) => {
|
||||
let mut candidate =
|
||||
classifier.classify_override(ChunkContext::ClassBody, inner, source)?;
|
||||
candidate.range_start_byte = node.start_byte();
|
||||
candidate.range_start_line = node.start_position().row + 1;
|
||||
candidate.checksum_start_byte = node.start_byte();
|
||||
Some(candidate)
|
||||
},
|
||||
None => Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::Template,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
source,
|
||||
)),
|
||||
}
|
||||
},
|
||||
|
||||
// ── Types ──
|
||||
"type_alias_declaration" => Some(named_candidate(node, ChunkKind::Type, source, None)),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{ClassifierTables, LangClassifier},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct ClojureClassifier;
|
||||
|
||||
@@ -52,24 +56,23 @@ fn classify_form<'t>(node: Node<'t>, source: &str, at_root: bool) -> RawChunkCan
|
||||
}
|
||||
|
||||
impl LangClassifier for ClojureClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
"list_lit" => Some(classify_form(node, source, true)),
|
||||
_ => None,
|
||||
}
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
static TABLES: ClassifierTables = ClassifierTables {
|
||||
root: &[],
|
||||
class: &[],
|
||||
function: &[],
|
||||
structural_overrides: super::classify::StructuralOverrides::EMPTY,
|
||||
};
|
||||
&TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
"list_lit" => Some(classify_form(node, source, false)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
"list_lit" => Some(classify_form(node, source, false)),
|
||||
_ => None,
|
||||
}
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
(node.kind() == "list_lit")
|
||||
.then(|| classify_form(node, source, matches!(context, ChunkContext::Root)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{ClassifierTables, LangClassifier, StructuralOverrides},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct CMakeClassifier;
|
||||
|
||||
@@ -129,24 +133,42 @@ fn classify_if_child<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandida
|
||||
}
|
||||
|
||||
impl LangClassifier for CMakeClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_definition(node, source).or_else(|| classify_command(node, source))
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
static TABLES: ClassifierTables = ClassifierTables {
|
||||
root: &[],
|
||||
class: &[],
|
||||
function: &[],
|
||||
structural_overrides: StructuralOverrides {
|
||||
extra_trivia: &[
|
||||
"endif_command",
|
||||
"endforeach_command",
|
||||
"endwhile_command",
|
||||
"endfunction_command",
|
||||
"endmacro_command",
|
||||
],
|
||||
preserved_trivia: &[],
|
||||
extra_root_wrappers: &[],
|
||||
preserved_root_wrappers: &[],
|
||||
absorbable_attrs: &[],
|
||||
},
|
||||
};
|
||||
&TABLES
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_definition(node, source)
|
||||
.or_else(|| classify_if_child(node, source))
|
||||
.or_else(|| classify_command(node, source))
|
||||
}
|
||||
|
||||
fn is_trivia(&self, kind: &str) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
"endif_command"
|
||||
| "endforeach_command"
|
||||
| "endwhile_command"
|
||||
| "endfunction_command"
|
||||
| "endmacro_command"
|
||||
)
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match context {
|
||||
ChunkContext::Root => {
|
||||
classify_definition(node, source).or_else(|| classify_command(node, source))
|
||||
},
|
||||
ChunkContext::FunctionBody => classify_definition(node, source)
|
||||
.or_else(|| classify_if_child(node, source))
|
||||
.or_else(|| classify_command(node, source)),
|
||||
ChunkContext::ClassBody => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,162 +2,302 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, defaults::classify_var_decl, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{
|
||||
ClassifierTables, LangClassifier, NamingMode, RecurseMode, RuleStyle, StructuralOverrides,
|
||||
semantic_rule,
|
||||
},
|
||||
common::*,
|
||||
defaults::classify_var_decl,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct CSharpJavaClassifier;
|
||||
|
||||
const CSHARP_JAVA_ROOT_RULES: &[super::classify::SemanticRule] = &[
|
||||
// ── Imports ──
|
||||
semantic_rule(
|
||||
"import_declaration",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"using_directive",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"package_declaration",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"namespace_statement",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
// ── Functions ──
|
||||
semantic_rule(
|
||||
"method_declaration",
|
||||
ChunkKind::Method,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"function_declaration",
|
||||
ChunkKind::Function,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"function_definition",
|
||||
ChunkKind::Function,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
// ── Constructors ──
|
||||
semantic_rule(
|
||||
"constructor_declaration",
|
||||
ChunkKind::Constructor,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
// ── Containers ──
|
||||
semantic_rule(
|
||||
"class_declaration",
|
||||
ChunkKind::Class,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"interface_declaration",
|
||||
ChunkKind::Iface,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"enum_declaration",
|
||||
ChunkKind::Enum,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"struct_declaration",
|
||||
ChunkKind::Struct,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"record_declaration",
|
||||
ChunkKind::Struct,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"namespace_declaration",
|
||||
ChunkKind::Module,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"file_scoped_namespace_declaration",
|
||||
ChunkKind::Module,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
// ── Types ──
|
||||
semantic_rule(
|
||||
"type_alias_declaration",
|
||||
ChunkKind::Type,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
// ── Declarations ──
|
||||
semantic_rule(
|
||||
"property_declaration",
|
||||
ChunkKind::Declarations,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"state_variable_declaration",
|
||||
ChunkKind::Declarations,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
// ── Statements ──
|
||||
semantic_rule(
|
||||
"expression_statement",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
];
|
||||
|
||||
const CSHARP_JAVA_CLASS_RULES: &[super::classify::SemanticRule] = &[
|
||||
// ── Containers ──
|
||||
semantic_rule(
|
||||
"class_declaration",
|
||||
ChunkKind::Class,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"interface_declaration",
|
||||
ChunkKind::Iface,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"enum_declaration",
|
||||
ChunkKind::Enum,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"struct_declaration",
|
||||
ChunkKind::Struct,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"record_declaration",
|
||||
ChunkKind::Struct,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"namespace_declaration",
|
||||
ChunkKind::Module,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"file_scoped_namespace_declaration",
|
||||
ChunkKind::Module,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
// ── Static blocks ──
|
||||
semantic_rule(
|
||||
"class_static_block",
|
||||
ChunkKind::StaticInit,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
];
|
||||
|
||||
const CSHARP_JAVA_TABLES: ClassifierTables = ClassifierTables {
|
||||
root: CSHARP_JAVA_ROOT_RULES,
|
||||
class: CSHARP_JAVA_CLASS_RULES,
|
||||
function: &[],
|
||||
structural_overrides: StructuralOverrides::EMPTY,
|
||||
};
|
||||
|
||||
impl LangClassifier for CSharpJavaClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// ── Imports ──
|
||||
"import_declaration"
|
||||
| "using_directive"
|
||||
| "package_declaration"
|
||||
| "namespace_statement" => Some(group_candidate(node, ChunkKind::Imports, source)),
|
||||
|
||||
// ── Functions ──
|
||||
"method_declaration" => Some(named_candidate(
|
||||
node,
|
||||
ChunkKind::Method,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)),
|
||||
"function_declaration" | "function_definition" => Some(named_candidate(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)),
|
||||
|
||||
// ── Constructors ──
|
||||
"constructor_declaration" => Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Constructor,
|
||||
None,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)),
|
||||
|
||||
// ── Containers ──
|
||||
"class_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Class, source, recurse_class(node)))
|
||||
},
|
||||
"interface_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Iface, source, recurse_interface(node)))
|
||||
},
|
||||
"enum_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Enum, source, recurse_enum(node)))
|
||||
},
|
||||
"namespace_declaration" | "file_scoped_namespace_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Module, source, recurse_class(node)))
|
||||
},
|
||||
"struct_declaration" | "record_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Struct, source, recurse_class(node)))
|
||||
},
|
||||
|
||||
// ── Types ──
|
||||
"type_alias_declaration" => {
|
||||
Some(named_candidate(node, ChunkKind::Type, source, recurse_class(node)))
|
||||
},
|
||||
|
||||
// ── Variables / assignments ──
|
||||
"variable_declaration" | "lexical_declaration" => Some(classify_var_decl(node, source)),
|
||||
"property_declaration" | "state_variable_declaration" => {
|
||||
Some(group_candidate(node, ChunkKind::Declarations, source))
|
||||
},
|
||||
|
||||
// ── Control flow (top-level scripts) ──
|
||||
"if_statement" | "switch_statement" | "switch_expression" | "for_statement"
|
||||
| "foreach_statement" | "while_statement" | "do_statement" | "try_statement" => {
|
||||
Some(classify_function_csharp_java(node, source))
|
||||
},
|
||||
|
||||
// ── Statements ──
|
||||
"expression_statement" => Some(group_candidate(node, ChunkKind::Statements, source)),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
&CSHARP_JAVA_TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// ── Container declarations (inside namespace/class bodies) ──
|
||||
"class_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Class, source, recurse_class(node)))
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match context {
|
||||
ChunkContext::Root => match node.kind() {
|
||||
// ── Variables / assignments ──
|
||||
"variable_declaration" | "lexical_declaration" => Some(classify_var_decl(node, source)),
|
||||
// ── Control flow (top-level scripts) ──
|
||||
"if_statement" | "switch_statement" | "switch_expression" | "for_statement"
|
||||
| "foreach_statement" | "while_statement" | "do_statement" | "try_statement" => {
|
||||
Some(classify_function_csharp_java(node, source))
|
||||
},
|
||||
_ => None,
|
||||
},
|
||||
"interface_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Iface, source, recurse_interface(node)))
|
||||
ChunkContext::ClassBody => match node.kind() {
|
||||
// ── Methods (conditional constructor detection) ──
|
||||
"method_declaration" | "function_declaration" | "function_definition" => {
|
||||
let name =
|
||||
extract_identifier(node, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
if name == "constructor" {
|
||||
Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Constructor,
|
||||
None,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
))
|
||||
} else {
|
||||
Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
Some(name),
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
))
|
||||
}
|
||||
},
|
||||
// ── Constructors ──
|
||||
"constructor_declaration" | "secondary_constructor" => Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Constructor,
|
||||
None,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)),
|
||||
// ── Fields ──
|
||||
"field_declaration"
|
||||
| "property_declaration"
|
||||
| "constant_declaration"
|
||||
| "event_field_declaration" => Some(match extract_field_name(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Field, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Fields, source),
|
||||
}),
|
||||
// ── Enum members ──
|
||||
"enum_member_declaration" | "enum_constant" | "enum_entry" => {
|
||||
Some(match extract_identifier(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Variant, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Variants, source),
|
||||
})
|
||||
},
|
||||
_ => None,
|
||||
},
|
||||
"enum_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Enum, source, recurse_enum(node)))
|
||||
},
|
||||
"struct_declaration" | "record_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Struct, source, recurse_class(node)))
|
||||
},
|
||||
"namespace_declaration" | "file_scoped_namespace_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Module, source, recurse_class(node)))
|
||||
},
|
||||
|
||||
// ── Methods ──
|
||||
"method_declaration" | "function_declaration" | "function_definition" => {
|
||||
let name = extract_identifier(node, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
if name == "constructor" {
|
||||
Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Constructor,
|
||||
None,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
))
|
||||
} else {
|
||||
Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
Some(name),
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
))
|
||||
}
|
||||
},
|
||||
|
||||
// ── Constructors ──
|
||||
"constructor_declaration" | "secondary_constructor" => Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Constructor,
|
||||
None,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)),
|
||||
|
||||
// ── Fields ──
|
||||
"field_declaration"
|
||||
| "property_declaration"
|
||||
| "constant_declaration"
|
||||
| "event_field_declaration" => Some(match extract_field_name(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Field, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Fields, source),
|
||||
}),
|
||||
|
||||
// ── Enum members ──
|
||||
"enum_member_declaration" | "enum_constant" | "enum_entry" => {
|
||||
Some(match extract_identifier(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Variant, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Variants, source),
|
||||
})
|
||||
},
|
||||
|
||||
// ── Static blocks ──
|
||||
"class_static_block" => {
|
||||
Some(make_kind_chunk(node, ChunkKind::StaticInit, None, source, None))
|
||||
},
|
||||
|
||||
_ => None,
|
||||
ChunkContext::FunctionBody => Some(classify_function_csharp_java(node, source)),
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(classify_function_csharp_java(node, source))
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the variable name from a field/constant declaration.
|
||||
|
||||
@@ -2,10 +2,47 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{
|
||||
ClassifierTables, LangClassifier, NamingMode, RecurseMode, RuleStyle, StructuralOverrides,
|
||||
semantic_rule,
|
||||
},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct CssClassifier;
|
||||
|
||||
const CSS_SHARED_RULES: &[super::classify::SemanticRule] = &[
|
||||
semantic_rule(
|
||||
"keyframe_block",
|
||||
ChunkKind::Frame,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::SelfNode(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"declaration",
|
||||
ChunkKind::Fields,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
];
|
||||
|
||||
const CSS_TABLES: ClassifierTables = ClassifierTables {
|
||||
root: CSS_SHARED_RULES,
|
||||
class: CSS_SHARED_RULES,
|
||||
function: &[],
|
||||
structural_overrides: StructuralOverrides {
|
||||
extra_trivia: &[],
|
||||
preserved_trivia: &[],
|
||||
extra_root_wrappers: &["stylesheet"],
|
||||
preserved_root_wrappers: &[],
|
||||
absorbable_attrs: &[],
|
||||
},
|
||||
};
|
||||
|
||||
/// Extract a CSS selector name from a `rule_set` or `at_rule` node.
|
||||
///
|
||||
/// Tries known child kinds first (`selectors`, `selector_query`, `identifier`),
|
||||
@@ -69,23 +106,19 @@ fn classify_css_node<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandida
|
||||
}
|
||||
|
||||
impl LangClassifier for CssClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_css_node(node, source)
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
&CSS_TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_css_node(node, source)
|
||||
}
|
||||
|
||||
fn classify_function<'t>(
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
_node: Node<'t>,
|
||||
_source: &str,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
if matches!(context, ChunkContext::Root | ChunkContext::ClassBody) {
|
||||
return classify_css_node(node, source);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn is_root_wrapper(&self, kind: &str) -> bool {
|
||||
kind == "stylesheet"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,25 +2,184 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{
|
||||
ClassifierTables, LangClassifier, NamingMode, RecurseMode, RuleStyle, StructuralOverrides,
|
||||
semantic_rule,
|
||||
},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct DataFormatsClassifier;
|
||||
|
||||
const DATA_FORMAT_STRUCTURAL_OVERRIDES: StructuralOverrides = StructuralOverrides {
|
||||
extra_trivia: &["bare_key", "quoted_key", "dotted_key"],
|
||||
preserved_trivia: &[],
|
||||
extra_root_wrappers: &[
|
||||
"array",
|
||||
"block_mapping",
|
||||
"block_node",
|
||||
"block_sequence",
|
||||
"document",
|
||||
"flow_mapping",
|
||||
"flow_node",
|
||||
"flow_sequence",
|
||||
"object",
|
||||
"stream",
|
||||
],
|
||||
preserved_root_wrappers: &[],
|
||||
absorbable_attrs: &[],
|
||||
};
|
||||
|
||||
const DATA_FORMAT_ROOT_RULES: &[super::classify::SemanticRule] = &[
|
||||
semantic_rule(
|
||||
"inline_table",
|
||||
ChunkKind::Table,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::SelfNode(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"object",
|
||||
ChunkKind::Object,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::SelfNode(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"array",
|
||||
ChunkKind::Array,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::SelfNode(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"block_mapping",
|
||||
ChunkKind::Map,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::SelfNode(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"flow_mapping",
|
||||
ChunkKind::Map,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::SelfNode(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"block_sequence",
|
||||
ChunkKind::List,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::SelfNode(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"flow_sequence",
|
||||
ChunkKind::List,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::SelfNode(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"attribute",
|
||||
ChunkKind::Attr,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::ValueContainer,
|
||||
),
|
||||
];
|
||||
|
||||
const DATA_FORMAT_CLASS_RULES: &[super::classify::SemanticRule] = &[
|
||||
semantic_rule(
|
||||
"inline_table",
|
||||
ChunkKind::Table,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::SelfNode(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"object",
|
||||
ChunkKind::Object,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::SelfNode(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"array",
|
||||
ChunkKind::Array,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::SelfNode(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"block_mapping",
|
||||
ChunkKind::Map,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::SelfNode(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"flow_mapping",
|
||||
ChunkKind::Map,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::SelfNode(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"block_sequence",
|
||||
ChunkKind::List,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::SelfNode(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"flow_sequence",
|
||||
ChunkKind::List,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::SelfNode(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"block_sequence_item",
|
||||
ChunkKind::Item,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"attribute",
|
||||
ChunkKind::Attr,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::ValueContainer,
|
||||
),
|
||||
];
|
||||
|
||||
const DATA_FORMAT_TABLES: ClassifierTables = ClassifierTables {
|
||||
root: DATA_FORMAT_ROOT_RULES,
|
||||
class: DATA_FORMAT_CLASS_RULES,
|
||||
function: &[],
|
||||
structural_overrides: DATA_FORMAT_STRUCTURAL_OVERRIDES,
|
||||
};
|
||||
|
||||
impl LangClassifier for DataFormatsClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_data_node(node, source, true)
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
&DATA_FORMAT_TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_data_node(node, source, false)
|
||||
}
|
||||
|
||||
fn classify_function<'t>(
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
_node: Node<'t>,
|
||||
_source: &str,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
None
|
||||
match context {
|
||||
ChunkContext::Root => classify_data_node(node, source, true),
|
||||
ChunkContext::ClassBody => classify_data_node(node, source, false),
|
||||
ChunkContext::FunctionBody => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn preserve_children(
|
||||
@@ -64,70 +223,30 @@ fn classify_data_node<'t>(
|
||||
Some(candidate)
|
||||
},
|
||||
// TOML tables
|
||||
"table" => Some(container_candidate(
|
||||
node,
|
||||
ChunkKind::Table,
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::ClassBody)),
|
||||
)),
|
||||
"table" => {
|
||||
let name = extract_toml_table_name(node, source);
|
||||
Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Table,
|
||||
name,
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::ClassBody)),
|
||||
))
|
||||
},
|
||||
// TOML array tables
|
||||
"table_array_element" => Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::Table,
|
||||
"table_array".to_string(),
|
||||
extract_toml_table_name(node, source).unwrap_or_else(|| "table_array".to_string()),
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
Some(recurse_self(node, ChunkContext::ClassBody)),
|
||||
source,
|
||||
)),
|
||||
// TOML inline tables
|
||||
"inline_table" => Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Table,
|
||||
None,
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::ClassBody)),
|
||||
)),
|
||||
// JSON objects
|
||||
"object" => Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Object,
|
||||
None,
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::ClassBody)),
|
||||
)),
|
||||
// JSON arrays
|
||||
"array" => Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Array,
|
||||
None,
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::ClassBody)),
|
||||
)),
|
||||
// YAML block/flow mappings
|
||||
"block_mapping" | "flow_mapping" => Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Map,
|
||||
None,
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::ClassBody)),
|
||||
)),
|
||||
// YAML block/flow sequences
|
||||
"block_sequence" | "flow_sequence" => Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::List,
|
||||
None,
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::ClassBody)),
|
||||
)),
|
||||
// YAML sequence items (only when nested, not at root level)
|
||||
"block_sequence_item" if !is_root => {
|
||||
Some(positional_candidate(node, ChunkKind::Item, source))
|
||||
},
|
||||
// Nix-style attributes that appear in data contexts
|
||||
"attribute" => {
|
||||
Some(named_candidate(node, ChunkKind::Attr, source, recurse_value_container(node)))
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -143,6 +262,11 @@ fn extract_pair_key(node: Node<'_>, source: &str) -> Option<String> {
|
||||
sanitize_identifier(unquote_text(node_text(source, key.start_byte(), key.end_byte())).as_str())
|
||||
}
|
||||
|
||||
fn extract_toml_table_name(node: Node<'_>, source: &str) -> Option<String> {
|
||||
let key = child_by_kind(node, &["dotted_key", "bare_key", "quoted_key"])?;
|
||||
sanitize_identifier(node_text(source, key.start_byte(), key.end_byte()))
|
||||
}
|
||||
|
||||
/// Extract key from a YAML `block_mapping_pair` or `flow_pair` node.
|
||||
/// Descends into the key to find the first scalar child for complex keys.
|
||||
fn extract_yaml_key(node: Node<'_>, source: &str) -> Option<String> {
|
||||
|
||||
@@ -2,10 +2,106 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{
|
||||
ClassifierTables, LangClassifier, NamingMode, RecurseMode, RuleStyle, semantic_rule,
|
||||
},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct DockerfileClassifier;
|
||||
|
||||
const DOCKERFILE_ROOT_RULES: &[super::classify::SemanticRule] = &[
|
||||
semantic_rule(
|
||||
"run_instruction",
|
||||
ChunkKind::Cmd,
|
||||
RuleStyle::Named,
|
||||
NamingMode::SanitizedKind,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"cmd_instruction",
|
||||
ChunkKind::Cmd,
|
||||
RuleStyle::Named,
|
||||
NamingMode::SanitizedKind,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"entrypoint_instruction",
|
||||
ChunkKind::Cmd,
|
||||
RuleStyle::Named,
|
||||
NamingMode::SanitizedKind,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"copy_instruction",
|
||||
ChunkKind::Copy,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"add_instruction",
|
||||
ChunkKind::Add,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"workdir_instruction",
|
||||
ChunkKind::Workdir,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"expose_instruction",
|
||||
ChunkKind::Expose,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"user_instruction",
|
||||
ChunkKind::User,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
];
|
||||
|
||||
const DOCKERFILE_FUNCTION_RULES: &[super::classify::SemanticRule] = &[
|
||||
semantic_rule(
|
||||
"cmd_instruction",
|
||||
ChunkKind::Cmd,
|
||||
RuleStyle::Named,
|
||||
NamingMode::SanitizedKind,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"shell_command",
|
||||
ChunkKind::Shell,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"json_string_array",
|
||||
ChunkKind::Argv,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
];
|
||||
|
||||
const DOCKERFILE_TABLES: ClassifierTables = ClassifierTables {
|
||||
root: DOCKERFILE_ROOT_RULES,
|
||||
class: &[],
|
||||
function: DOCKERFILE_FUNCTION_RULES,
|
||||
structural_overrides: super::classify::StructuralOverrides::EMPTY,
|
||||
};
|
||||
|
||||
fn child_text<'a>(source: &'a str, node: Node<'_>) -> &'a str {
|
||||
node_text(source, node.start_byte(), node.end_byte())
|
||||
}
|
||||
@@ -41,78 +137,47 @@ fn extract_arg_name(node: Node<'_>, source: &str) -> Option<String> {
|
||||
first_named_child(node).and_then(|name| sanitize_identifier(child_text(source, name)))
|
||||
}
|
||||
|
||||
fn recurse_command(node: Node<'_>) -> Option<RecurseSpec<'_>> {
|
||||
recurse_into(node, ChunkContext::FunctionBody, &[], &["shell_command", "json_string_array"])
|
||||
}
|
||||
|
||||
fn classify_command_instruction<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
let chunk_name = match node.kind() {
|
||||
"run_instruction" => "run",
|
||||
"cmd_instruction" => "cmd",
|
||||
"entrypoint_instruction" => "entrypoint",
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::Cmd,
|
||||
chunk_name.to_string(),
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
recurse_command(node),
|
||||
source,
|
||||
))
|
||||
}
|
||||
|
||||
impl LangClassifier for DockerfileClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
"from_instruction" => {
|
||||
let name = extract_stage_name(node, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
Some(make_kind_chunk(node, ChunkKind::Stage, Some(name), source, None))
|
||||
},
|
||||
"arg_instruction" => {
|
||||
let name = extract_arg_name(node, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
Some(make_kind_chunk(node, ChunkKind::Arg, Some(name), source, None))
|
||||
},
|
||||
"env_instruction" => {
|
||||
let name = extract_pair_key(node, "env_pair", source)
|
||||
.unwrap_or_else(|| "anonymous".to_string());
|
||||
Some(make_kind_chunk(node, ChunkKind::Env, Some(name), source, None))
|
||||
},
|
||||
"label_instruction" => {
|
||||
let name = extract_pair_key(node, "label_pair", source)
|
||||
.unwrap_or_else(|| "anonymous".to_string());
|
||||
Some(make_kind_chunk(node, ChunkKind::Label, Some(name), source, None))
|
||||
},
|
||||
"run_instruction" | "cmd_instruction" | "entrypoint_instruction" => {
|
||||
classify_command_instruction(node, source)
|
||||
},
|
||||
"healthcheck_instruction" => Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Healthcheck,
|
||||
None,
|
||||
source,
|
||||
recurse_into(node, ChunkContext::FunctionBody, &[], &["cmd_instruction"]),
|
||||
)),
|
||||
"copy_instruction" => Some(group_candidate(node, ChunkKind::Copy, source)),
|
||||
"add_instruction" => Some(group_candidate(node, ChunkKind::Add, source)),
|
||||
"workdir_instruction" => Some(group_candidate(node, ChunkKind::Workdir, source)),
|
||||
"expose_instruction" => Some(group_candidate(node, ChunkKind::Expose, source)),
|
||||
"user_instruction" => Some(group_candidate(node, ChunkKind::User, source)),
|
||||
_ => None,
|
||||
}
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
&DOCKERFILE_TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, _node: Node<'t>, _source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
"cmd_instruction" => classify_command_instruction(node, source),
|
||||
"shell_command" => Some(group_candidate(node, ChunkKind::Shell, source)),
|
||||
"json_string_array" => Some(group_candidate(node, ChunkKind::Argv, source)),
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match context {
|
||||
ChunkContext::Root => match node.kind() {
|
||||
"from_instruction" => {
|
||||
let name =
|
||||
extract_stage_name(node, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
Some(make_kind_chunk(node, ChunkKind::Stage, Some(name), source, None))
|
||||
},
|
||||
"arg_instruction" => {
|
||||
let name = extract_arg_name(node, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
Some(make_kind_chunk(node, ChunkKind::Arg, Some(name), source, None))
|
||||
},
|
||||
"env_instruction" => {
|
||||
let name = extract_pair_key(node, "env_pair", source)
|
||||
.unwrap_or_else(|| "anonymous".to_string());
|
||||
Some(make_kind_chunk(node, ChunkKind::Env, Some(name), source, None))
|
||||
},
|
||||
"label_instruction" => {
|
||||
let name = extract_pair_key(node, "label_pair", source)
|
||||
.unwrap_or_else(|| "anonymous".to_string());
|
||||
Some(make_kind_chunk(node, ChunkKind::Label, Some(name), source, None))
|
||||
},
|
||||
"healthcheck_instruction" => Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Healthcheck,
|
||||
None,
|
||||
source,
|
||||
recurse_into(node, ChunkContext::FunctionBody, &[], &["cmd_instruction"]),
|
||||
)),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{ClassifierTables, LangClassifier, StructuralOverrides},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct ElixirClassifier;
|
||||
|
||||
@@ -128,31 +132,28 @@ fn call_name(node: Node<'_>, source: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
impl LangClassifier for ElixirClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
"call" => Some(classify_call(node, source, true)),
|
||||
_ => None,
|
||||
}
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
static TABLES: ClassifierTables = ClassifierTables {
|
||||
root: &[],
|
||||
class: &[],
|
||||
function: &[],
|
||||
structural_overrides: StructuralOverrides {
|
||||
extra_trivia: &["unary_operator"],
|
||||
preserved_trivia: &[],
|
||||
extra_root_wrappers: &[],
|
||||
preserved_root_wrappers: &[],
|
||||
absorbable_attrs: &[],
|
||||
},
|
||||
};
|
||||
&TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
"call" => Some(classify_call(node, source, false)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
"call" => Some(classify_call(node, source, false)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_trivia(&self, kind: &str) -> bool {
|
||||
// `@doc`, `@spec`, `@impl`, `@type`, `@moduledoc`, etc. are all
|
||||
// `unary_operator` nodes in the Elixir grammar (operator `@`).
|
||||
// Treat them as trivia so they get absorbed into the next chunk.
|
||||
kind == "unary_operator"
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
(node.kind() == "call").then(|| classify_call(node, source, context == ChunkContext::Root))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,14 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{
|
||||
ClassifierTables, LangClassifier, NamingMode, RecurseMode, RuleStyle, StructuralOverrides,
|
||||
semantic_rule,
|
||||
},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct ErlangClassifier;
|
||||
|
||||
@@ -51,129 +58,185 @@ fn recurse_clause_body(node: Node<'_>) -> Option<RecurseSpec<'_>> {
|
||||
}
|
||||
|
||||
impl LangClassifier for ErlangClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
"module_attribute" => {
|
||||
make_kind_chunk(node, ChunkKind::Module, erlang_name(node, source), source, None)
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
static TABLES: ClassifierTables = ClassifierTables {
|
||||
root: &[
|
||||
semantic_rule(
|
||||
"export_attribute",
|
||||
ChunkKind::Exports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"export_type_attribute",
|
||||
ChunkKind::Exports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"import_attribute",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"pp_include",
|
||||
ChunkKind::Includes,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"pp_include_lib",
|
||||
ChunkKind::Includes,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
],
|
||||
class: &[],
|
||||
function: &[],
|
||||
structural_overrides: StructuralOverrides {
|
||||
extra_trivia: &[],
|
||||
preserved_trivia: &[],
|
||||
extra_root_wrappers: &[],
|
||||
preserved_root_wrappers: &[],
|
||||
absorbable_attrs: &["spec"],
|
||||
},
|
||||
"export_attribute" | "export_type_attribute" => {
|
||||
group_candidate(node, ChunkKind::Exports, source)
|
||||
},
|
||||
"import_attribute" => group_candidate(node, ChunkKind::Imports, source),
|
||||
"pp_include" | "pp_include_lib" => group_candidate(node, ChunkKind::Includes, source),
|
||||
"pp_define" => {
|
||||
make_kind_chunk(node, ChunkKind::Macro, erlang_name(node, source), source, None)
|
||||
},
|
||||
"record_decl" => make_candidate(
|
||||
node,
|
||||
ChunkKind::Struct,
|
||||
format!("record_{}", erlang_name(node, source)?),
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
Some(recurse_self(node, ChunkContext::ClassBody)),
|
||||
source,
|
||||
),
|
||||
"type_alias" => {
|
||||
make_kind_chunk(node, ChunkKind::Type, erlang_name(node, source), source, None)
|
||||
},
|
||||
// The Erlang grammar exposes each top-level clause as its own `fun_decl`.
|
||||
// Keep that shape instead of inventing a synthetic merged function node.
|
||||
"fun_decl" => make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
erlang_name(node, source),
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
),
|
||||
"spec" => return None,
|
||||
_ => return None,
|
||||
})
|
||||
};
|
||||
&TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
"record_field" => {
|
||||
make_kind_chunk(node, ChunkKind::Field, erlang_name(node, source), source, None)
|
||||
},
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
"function_clause" => make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Clause,
|
||||
erlang_name(node, source),
|
||||
source,
|
||||
recurse_clause_body(node),
|
||||
),
|
||||
"fun_clause" | "cr_clause" => make_candidate(
|
||||
node,
|
||||
ChunkKind::Clause,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
recurse_clause_body(node),
|
||||
source,
|
||||
),
|
||||
"receive_after" => make_candidate(
|
||||
node,
|
||||
ChunkKind::After,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
recurse_clause_body(node),
|
||||
source,
|
||||
),
|
||||
"catch_clause" => make_candidate(
|
||||
node,
|
||||
ChunkKind::Catch,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
recurse_clause_body(node),
|
||||
source,
|
||||
),
|
||||
"receive_expr" => make_candidate(
|
||||
node,
|
||||
ChunkKind::Receive,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
source,
|
||||
),
|
||||
"case_expr" => make_candidate(
|
||||
node,
|
||||
ChunkKind::Case,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
source,
|
||||
),
|
||||
"try_expr" => make_candidate(
|
||||
node,
|
||||
ChunkKind::Try,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
source,
|
||||
),
|
||||
"anonymous_fun" => make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
Some("anonymous".to_string()),
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn is_absorbable_attr(&self, kind: &str) -> bool {
|
||||
kind == "spec"
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match context {
|
||||
ChunkContext::Root => classify_erlang_root(node, source),
|
||||
ChunkContext::ClassBody => classify_erlang_class(node, source),
|
||||
ChunkContext::FunctionBody => classify_erlang_function(node, source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_erlang_root<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
"module_attribute" => {
|
||||
make_kind_chunk(node, ChunkKind::Module, erlang_name(node, source), source, None)
|
||||
},
|
||||
"pp_define" => {
|
||||
make_kind_chunk(node, ChunkKind::Macro, erlang_name(node, source), source, None)
|
||||
},
|
||||
"record_decl" => make_candidate(
|
||||
node,
|
||||
ChunkKind::Struct,
|
||||
format!("record_{}", erlang_name(node, source)?),
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
Some(recurse_self(node, ChunkContext::ClassBody)),
|
||||
source,
|
||||
),
|
||||
"type_alias" => {
|
||||
make_kind_chunk(node, ChunkKind::Type, erlang_name(node, source), source, None)
|
||||
},
|
||||
// The Erlang grammar exposes each top-level clause as its own `fun_decl`.
|
||||
// Keep that shape instead of inventing a synthetic merged function node.
|
||||
"fun_decl" => make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
erlang_name(node, source),
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
),
|
||||
"spec" => return None,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn classify_erlang_class<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
"record_field" => {
|
||||
make_kind_chunk(node, ChunkKind::Field, erlang_name(node, source), source, None)
|
||||
},
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn classify_erlang_function<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
"function_clause" => make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Clause,
|
||||
erlang_name(node, source),
|
||||
source,
|
||||
recurse_clause_body(node),
|
||||
),
|
||||
"fun_clause" | "cr_clause" => make_candidate(
|
||||
node,
|
||||
ChunkKind::Clause,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
recurse_clause_body(node),
|
||||
source,
|
||||
),
|
||||
"receive_after" => make_candidate(
|
||||
node,
|
||||
ChunkKind::After,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
recurse_clause_body(node),
|
||||
source,
|
||||
),
|
||||
"catch_clause" => make_candidate(
|
||||
node,
|
||||
ChunkKind::Catch,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
recurse_clause_body(node),
|
||||
source,
|
||||
),
|
||||
"receive_expr" => make_candidate(
|
||||
node,
|
||||
ChunkKind::Receive,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
source,
|
||||
),
|
||||
"case_expr" => make_candidate(
|
||||
node,
|
||||
ChunkKind::Case,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
source,
|
||||
),
|
||||
"try_expr" => make_candidate(
|
||||
node,
|
||||
ChunkKind::Try,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
source,
|
||||
),
|
||||
"anonymous_fun" => make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
Some("anonymous".to_string()),
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,143 +1,250 @@
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{
|
||||
ClassifierTables, LangClassifier, NamingMode, RecurseMode, RuleStyle, StructuralOverrides,
|
||||
semantic_rule,
|
||||
},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct GoClassifier;
|
||||
|
||||
const ROOT_RULES: &[super::classify::SemanticRule] = &[
|
||||
// ── Imports / package ──
|
||||
semantic_rule(
|
||||
"import_declaration",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"package_clause",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
// ── Functions ──
|
||||
semantic_rule(
|
||||
"function_declaration",
|
||||
ChunkKind::Function,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"method_declaration",
|
||||
ChunkKind::Function,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
// ── Statements ──
|
||||
semantic_rule(
|
||||
"expression_statement",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"go_statement",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"defer_statement",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"send_statement",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
];
|
||||
|
||||
const CLASS_RULES: &[super::classify::SemanticRule] = &[
|
||||
// ── Methods ──
|
||||
semantic_rule(
|
||||
"method_spec",
|
||||
ChunkKind::Method,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::None,
|
||||
),
|
||||
// ── Field / method lists ──
|
||||
semantic_rule(
|
||||
"field_declaration_list",
|
||||
ChunkKind::Fields,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"method_spec_list",
|
||||
ChunkKind::Methods,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
];
|
||||
|
||||
const FUNCTION_RULES: &[super::classify::SemanticRule] = &[
|
||||
// ── Control flow ──
|
||||
semantic_rule(
|
||||
"if_statement",
|
||||
ChunkKind::If,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"for_statement",
|
||||
ChunkKind::For,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"switch_statement",
|
||||
ChunkKind::Switch,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"expression_switch_statement",
|
||||
ChunkKind::Switch,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"type_switch_statement",
|
||||
ChunkKind::Switch,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"select_statement",
|
||||
ChunkKind::Switch,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
// ── Statements ──
|
||||
semantic_rule(
|
||||
"go_statement",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"defer_statement",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"send_statement",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
];
|
||||
|
||||
const GO_TABLES: ClassifierTables = ClassifierTables {
|
||||
root: ROOT_RULES,
|
||||
class: CLASS_RULES,
|
||||
function: FUNCTION_RULES,
|
||||
structural_overrides: StructuralOverrides::EMPTY,
|
||||
};
|
||||
|
||||
impl LangClassifier for GoClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// ── Imports / package ──
|
||||
"import_declaration" | "package_clause" => {
|
||||
Some(group_candidate(node, ChunkKind::Imports, source))
|
||||
},
|
||||
|
||||
// ── Variables ──
|
||||
"const_declaration" | "var_declaration" | "short_var_declaration" => {
|
||||
Some(match extract_identifier(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Declarations, source),
|
||||
})
|
||||
},
|
||||
|
||||
// ── Functions ──
|
||||
"function_declaration" => Some(named_candidate(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)),
|
||||
"method_declaration" => Some(named_candidate(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)),
|
||||
|
||||
// ── Containers ──
|
||||
"type_declaration" => Some(classify_type_decl(node, source)),
|
||||
|
||||
// ── Control flow (top-level scripts) ──
|
||||
"if_statement"
|
||||
| "switch_statement"
|
||||
| "expression_switch_statement"
|
||||
| "type_switch_statement"
|
||||
| "select_statement"
|
||||
| "for_statement" => Some(classify_function_go(node, source)),
|
||||
|
||||
// ── Statements ──
|
||||
"expression_statement" | "go_statement" | "defer_statement" | "send_statement" => {
|
||||
Some(group_candidate(node, ChunkKind::Statements, source))
|
||||
},
|
||||
|
||||
_ => None,
|
||||
}
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
&GO_TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// ── Methods ──
|
||||
"method_spec" => Some(named_candidate(node, ChunkKind::Method, source, None)),
|
||||
|
||||
// ── Fields ──
|
||||
"field_declaration" | "embedded_field" => Some(match extract_identifier(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Field, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Fields, source),
|
||||
}),
|
||||
|
||||
// ── Field / method lists ──
|
||||
"field_declaration_list" => Some(group_candidate(node, ChunkKind::Fields, source)),
|
||||
"method_spec_list" => Some(group_candidate(node, ChunkKind::Methods, source)),
|
||||
|
||||
_ => None,
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match context {
|
||||
ChunkContext::Root => classify_root_custom(node, source),
|
||||
ChunkContext::ClassBody => classify_class_custom(node, source),
|
||||
ChunkContext::FunctionBody => classify_function_custom(node, source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// ── Control flow ──
|
||||
"if_statement" => Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::If,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
None,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
source,
|
||||
)),
|
||||
"switch_statement" | "expression_switch_statement" | "type_switch_statement" => {
|
||||
Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::Switch,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
None,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
source,
|
||||
))
|
||||
},
|
||||
"select_statement" => Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::Switch,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
None,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
source,
|
||||
)),
|
||||
fn classify_root_custom<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// ── Variables ──
|
||||
"const_declaration" | "var_declaration" | "short_var_declaration" => {
|
||||
Some(match extract_identifier(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Declarations, source),
|
||||
})
|
||||
},
|
||||
|
||||
// ── Loops ──
|
||||
"for_statement" => Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::For,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
None,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
source,
|
||||
)),
|
||||
// ── Containers ──
|
||||
"type_declaration" => Some(classify_type_decl(node, source)),
|
||||
|
||||
// ── Blocks ──
|
||||
"go_statement" | "defer_statement" | "send_statement" => {
|
||||
Some(group_candidate(node, ChunkKind::Statements, source))
|
||||
},
|
||||
// ── Control flow (top-level scripts) ──
|
||||
"if_statement"
|
||||
| "switch_statement"
|
||||
| "expression_switch_statement"
|
||||
| "type_switch_statement"
|
||||
| "select_statement"
|
||||
| "for_statement" => Some(classify_function_go(node, source)),
|
||||
|
||||
// ── Variables ──
|
||||
"short_var_declaration" | "var_declaration" | "const_declaration" => {
|
||||
let span = line_span(node.start_position().row + 1, node.end_position().row + 1);
|
||||
Some(if span > 1 {
|
||||
if let Some(name) = extract_identifier(node, source) {
|
||||
make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None)
|
||||
} else {
|
||||
group_from_sanitized(node, source)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_class_custom<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// ── Fields ──
|
||||
"field_declaration" | "embedded_field" => Some(match extract_identifier(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Field, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Fields, source),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_function_custom<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// ── Variables ──
|
||||
"short_var_declaration" | "var_declaration" | "const_declaration" => {
|
||||
let span = line_span(node.start_position().row + 1, node.end_position().row + 1);
|
||||
Some(if span > 1 {
|
||||
if let Some(name) = extract_identifier(node, source) {
|
||||
make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None)
|
||||
} else {
|
||||
group_from_sanitized(node, source)
|
||||
})
|
||||
},
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
group_from_sanitized(node, source)
|
||||
})
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,36 +2,48 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{ClassifierTables, LangClassifier, StructuralOverrides},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct GraphqlClassifier;
|
||||
|
||||
impl LangClassifier for GraphqlClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_graphql_root(node, source)
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
static TABLES: ClassifierTables = ClassifierTables {
|
||||
root: &[],
|
||||
class: &[],
|
||||
function: &[],
|
||||
structural_overrides: StructuralOverrides {
|
||||
extra_trivia: &["comma"],
|
||||
preserved_trivia: &[],
|
||||
extra_root_wrappers: &[
|
||||
"document",
|
||||
"definition",
|
||||
"type_system_definition",
|
||||
"type_definition",
|
||||
"executable_definition",
|
||||
],
|
||||
preserved_root_wrappers: &[],
|
||||
absorbable_attrs: &[],
|
||||
},
|
||||
};
|
||||
&TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_graphql_class(node, source)
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_graphql_function(node, source)
|
||||
}
|
||||
|
||||
fn is_root_wrapper(&self, kind: &str) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
"document"
|
||||
| "definition"
|
||||
| "type_system_definition"
|
||||
| "type_definition"
|
||||
| "executable_definition"
|
||||
)
|
||||
}
|
||||
|
||||
fn is_trivia(&self, kind: &str) -> bool {
|
||||
matches!(kind, "comma")
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match context {
|
||||
ChunkContext::Root => classify_graphql_root(node, source),
|
||||
ChunkContext::ClassBody => classify_graphql_class(node, source),
|
||||
ChunkContext::FunctionBody => classify_graphql_function(node, source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,136 +2,194 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{
|
||||
ClassifierTables, LangClassifier, NamingMode, RecurseMode, RuleStyle, semantic_rule,
|
||||
},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct HaskellScalaClassifier;
|
||||
|
||||
const HASKELL_SCALA_ROOT_RULES: &[super::classify::SemanticRule] = &[
|
||||
semantic_rule(
|
||||
"import_declaration",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"package_declaration",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"module",
|
||||
ChunkKind::Module,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"function_declaration",
|
||||
ChunkKind::Function,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"function_definition",
|
||||
ChunkKind::Function,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"class_definition",
|
||||
ChunkKind::Class,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"object_definition",
|
||||
ChunkKind::Module,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"trait_definition",
|
||||
ChunkKind::Iface,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"type_alias_declaration",
|
||||
ChunkKind::Type,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"type_item",
|
||||
ChunkKind::Type,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"variable_declaration",
|
||||
ChunkKind::Declarations,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"assignment",
|
||||
ChunkKind::Declarations,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"expression_statement",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
];
|
||||
|
||||
const HASKELL_SCALA_FUNCTION_RULES: &[super::classify::SemanticRule] = &[
|
||||
semantic_rule(
|
||||
"if_statement",
|
||||
ChunkKind::If,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"match_expression",
|
||||
ChunkKind::Match,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"for_expression",
|
||||
ChunkKind::Loop,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"while_expression",
|
||||
ChunkKind::Loop,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"block_expression",
|
||||
ChunkKind::Block,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
];
|
||||
|
||||
const HASKELL_SCALA_TABLES: ClassifierTables = ClassifierTables {
|
||||
root: HASKELL_SCALA_ROOT_RULES,
|
||||
class: &[],
|
||||
function: HASKELL_SCALA_FUNCTION_RULES,
|
||||
structural_overrides: super::classify::StructuralOverrides::EMPTY,
|
||||
};
|
||||
|
||||
impl LangClassifier for HaskellScalaClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
// ── Imports / packages ──
|
||||
"import_declaration" => group_candidate(node, ChunkKind::Imports, source),
|
||||
"package_declaration" => group_candidate(node, ChunkKind::Imports, source),
|
||||
|
||||
// ── Haskell module ──
|
||||
"module" => container_candidate(node, ChunkKind::Module, source, recurse_class(node)),
|
||||
|
||||
// ── Functions ──
|
||||
"function_declaration" => named_candidate(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
),
|
||||
"function_definition" => named_candidate(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
),
|
||||
|
||||
// ── Containers (Scala) ──
|
||||
"class_definition" => {
|
||||
container_candidate(node, ChunkKind::Class, source, recurse_class(node))
|
||||
},
|
||||
"object_definition" => {
|
||||
container_candidate(node, ChunkKind::Module, source, recurse_class(node))
|
||||
},
|
||||
"trait_definition" => {
|
||||
container_candidate(node, ChunkKind::Iface, source, recurse_interface(node))
|
||||
},
|
||||
|
||||
// ── Types ──
|
||||
"type_alias_declaration" | "type_item" => {
|
||||
named_candidate(node, ChunkKind::Type, source, recurse_class(node))
|
||||
},
|
||||
|
||||
// ── Variables / assignments ──
|
||||
"variable_declaration" | "assignment" => {
|
||||
group_candidate(node, ChunkKind::Declarations, source)
|
||||
},
|
||||
|
||||
// ── Statements ──
|
||||
"expression_statement" => group_candidate(node, ChunkKind::Statements, source),
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
&HASKELL_SCALA_TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
// ── Methods ──
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
if context != ChunkContext::ClassBody {
|
||||
return None;
|
||||
}
|
||||
|
||||
match node.kind() {
|
||||
"function_declaration" | "function_definition" | "method_definition" => {
|
||||
let name = extract_identifier(node, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
if name == "constructor" {
|
||||
make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Constructor,
|
||||
None,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)
|
||||
let kind = if name == "constructor" {
|
||||
ChunkKind::Constructor
|
||||
} else {
|
||||
make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
Some(name),
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)
|
||||
}
|
||||
ChunkKind::Function
|
||||
};
|
||||
let identifier = (kind != ChunkKind::Constructor).then_some(name);
|
||||
Some(make_kind_chunk(
|
||||
node,
|
||||
kind,
|
||||
identifier,
|
||||
source,
|
||||
resolve_recurse(node, ChunkContext::FunctionBody),
|
||||
))
|
||||
},
|
||||
|
||||
// ── Fields ──
|
||||
"variable_declaration" | "property_declaration" => {
|
||||
match extract_identifier(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Field, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Fields, source),
|
||||
}
|
||||
Some(extract_identifier(node, source).map_or_else(
|
||||
|| group_candidate(node, ChunkKind::Fields, source),
|
||||
|name| make_kind_chunk(node, ChunkKind::Field, Some(name), source, None),
|
||||
))
|
||||
},
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
let fn_recurse = || recurse_body(node, ChunkContext::FunctionBody);
|
||||
Some(match node.kind() {
|
||||
// ── Control flow ──
|
||||
"if_statement" => {
|
||||
make_candidate(node, ChunkKind::If, None, NameStyle::Named, None, fn_recurse(), source)
|
||||
},
|
||||
"match_expression" => make_candidate(
|
||||
node,
|
||||
ChunkKind::Match,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
None,
|
||||
fn_recurse(),
|
||||
source,
|
||||
),
|
||||
"for_expression" | "while_expression" => make_candidate(
|
||||
node,
|
||||
ChunkKind::Loop,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
None,
|
||||
fn_recurse(),
|
||||
source,
|
||||
),
|
||||
|
||||
// ── Blocks ──
|
||||
"block_expression" => make_candidate(
|
||||
node,
|
||||
ChunkKind::Block,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
None,
|
||||
fn_recurse(),
|
||||
source,
|
||||
),
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,31 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{
|
||||
ClassifierTables, LangClassifier, NamingMode, RecurseMode, RuleStyle, semantic_rule,
|
||||
},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct HtmlXmlClassifier;
|
||||
|
||||
const HTML_XML_SHARED_RULES: &[super::classify::SemanticRule] = &[semantic_rule(
|
||||
"text_node",
|
||||
ChunkKind::Text,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
)];
|
||||
|
||||
const HTML_XML_TABLES: ClassifierTables = ClassifierTables {
|
||||
root: HTML_XML_SHARED_RULES,
|
||||
class: HTML_XML_SHARED_RULES,
|
||||
function: &[],
|
||||
structural_overrides: super::classify::StructuralOverrides::EMPTY,
|
||||
};
|
||||
|
||||
/// Classify an element-like node as a container with tag semantics.
|
||||
///
|
||||
/// Uses `extract_markup_tag_name` directly because the shared
|
||||
@@ -50,19 +71,19 @@ fn extract_markup_tag_name(node: Node<'_>, source: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
impl LangClassifier for HtmlXmlClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_element(node, source)
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
&HTML_XML_TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_element(node, source)
|
||||
}
|
||||
|
||||
fn classify_function<'t>(
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
_node: Node<'t>,
|
||||
_source: &str,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
if matches!(context, ChunkContext::Root | ChunkContext::ClassBody) {
|
||||
return classify_element(node, source);
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,17 +7,36 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{ClassifierTables, LangClassifier},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct IniClassifier;
|
||||
|
||||
impl LangClassifier for IniClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_ini_root(node, source)
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
static TABLES: ClassifierTables = ClassifierTables {
|
||||
root: &[],
|
||||
class: &[],
|
||||
function: &[],
|
||||
structural_overrides: super::classify::StructuralOverrides::EMPTY,
|
||||
};
|
||||
&TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_ini_class(node, source)
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match context {
|
||||
ChunkContext::Root => classify_ini_root(node, source),
|
||||
ChunkContext::ClassBody => classify_ini_class(node, source),
|
||||
ChunkContext::FunctionBody => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{
|
||||
classify::LangClassifier, common::*, defaults::promote_assigned_expression, kind::ChunkKind,
|
||||
classify::{
|
||||
ClassifierTables, LangClassifier, NamingMode, RecurseMode, RuleStyle, semantic_rule,
|
||||
},
|
||||
common::*,
|
||||
defaults::promote_assigned_expression,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct JsTsClassifier;
|
||||
@@ -12,151 +17,191 @@ fn recurse_internal_module(node: Node<'_>) -> Option<RecurseSpec<'_>> {
|
||||
recurse_into(node, ChunkContext::ClassBody, &["body"], &["statement_block"])
|
||||
}
|
||||
|
||||
static JSTS_TABLES: ClassifierTables = ClassifierTables {
|
||||
root: &[
|
||||
semantic_rule(
|
||||
"import_statement",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"import_declaration",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"function_declaration",
|
||||
ChunkKind::Function,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"class_declaration",
|
||||
ChunkKind::Class,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"interface_declaration",
|
||||
ChunkKind::Interface,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"enum_declaration",
|
||||
ChunkKind::Enum,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"type_alias_declaration",
|
||||
ChunkKind::Type,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::None,
|
||||
),
|
||||
],
|
||||
class: &[
|
||||
semantic_rule(
|
||||
"constructor",
|
||||
ChunkKind::Constructor,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"class_static_block",
|
||||
ChunkKind::StaticInit,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"type_alias_declaration",
|
||||
ChunkKind::Type,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::None,
|
||||
),
|
||||
],
|
||||
function: &[],
|
||||
structural_overrides: super::classify::StructuralOverrides::EMPTY,
|
||||
};
|
||||
|
||||
impl LangClassifier for JsTsClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// ── Exports / decorators ──
|
||||
"export_statement" => Some(classify_export_statement(node, source)),
|
||||
"decorated_definition" => Some(classify_decorated(node, source)),
|
||||
|
||||
// ── Imports ──
|
||||
"import_statement" | "import_declaration" => {
|
||||
Some(group_candidate(node, ChunkKind::Imports, source))
|
||||
},
|
||||
|
||||
// ── Variables ──
|
||||
"lexical_declaration" | "variable_declaration" => {
|
||||
// Must handle here to ensure promote_assigned_expression runs.
|
||||
// The shared defaults classify_var_decl should do this, but we
|
||||
// need direct control for JS/TS patterns.
|
||||
Some(classify_var_decl_js(node, source))
|
||||
},
|
||||
|
||||
// ── Functions ──
|
||||
"function_declaration" => Some(named_candidate(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)),
|
||||
|
||||
// ── Containers ──
|
||||
"class_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Class, source, recurse_class(node)))
|
||||
},
|
||||
"interface_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Interface, source, recurse_interface(node)))
|
||||
},
|
||||
"enum_declaration" => {
|
||||
Some(container_candidate(node, ChunkKind::Enum, source, recurse_enum(node)))
|
||||
},
|
||||
"internal_module" => Some(container_candidate(
|
||||
node,
|
||||
ChunkKind::Module,
|
||||
source,
|
||||
recurse_internal_module(node),
|
||||
)),
|
||||
|
||||
// ── Types ──
|
||||
"type_alias_declaration" => Some(named_candidate(node, ChunkKind::Type, source, None)),
|
||||
|
||||
// ── Control flow at top level ──
|
||||
"if_statement" | "switch_statement" | "switch_expression" | "try_statement"
|
||||
| "for_statement" | "for_in_statement" | "for_of_statement" | "while_statement"
|
||||
| "do_statement" | "with_statement" => Some(classify_function_js(node, source)),
|
||||
|
||||
// ── Statements ──
|
||||
"expression_statement" => {
|
||||
// Unwrap `expression_statement` wrapping an `internal_module` (namespace).
|
||||
let inner = named_children(node)
|
||||
.into_iter()
|
||||
.find(|c| c.kind() == "internal_module");
|
||||
if let Some(ns) = inner {
|
||||
Some(container_candidate(ns, ChunkKind::Module, source, recurse_internal_module(ns)))
|
||||
} else {
|
||||
Some(group_candidate(node, ChunkKind::Statements, source))
|
||||
}
|
||||
},
|
||||
|
||||
_ => None,
|
||||
}
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
&JSTS_TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// ── Exports / decorators (re-exported members) ──
|
||||
"export_statement" => Some(classify_export_statement(node, source)),
|
||||
"decorated_definition" => Some(classify_decorated(node, source)),
|
||||
|
||||
// ── Variables ──
|
||||
"lexical_declaration" | "variable_declaration" => Some(classify_var_decl_js(node, source)),
|
||||
|
||||
// ── Constructor ──
|
||||
"constructor" => Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Constructor,
|
||||
None,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)),
|
||||
|
||||
// ── Methods ──
|
||||
"method_definition" | "method_signature" | "abstract_method_signature" => {
|
||||
let name = extract_identifier(node, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
if name == "constructor" {
|
||||
Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Constructor,
|
||||
None,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
))
|
||||
} else {
|
||||
Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
Some(name),
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
))
|
||||
}
|
||||
},
|
||||
|
||||
// ── Fields ──
|
||||
"public_field_definition"
|
||||
| "field_definition"
|
||||
| "property_definition"
|
||||
| "property_signature"
|
||||
| "property_declaration"
|
||||
| "abstract_class_field" => match extract_identifier(node, source) {
|
||||
Some(name) => Some(make_kind_chunk(node, ChunkKind::Field, Some(name), source, None)),
|
||||
None => Some(group_candidate(node, ChunkKind::Fields, source)),
|
||||
},
|
||||
|
||||
// ── Enum members ──
|
||||
"enum_assignment" | "enum_member_declaration" => match extract_identifier(node, source) {
|
||||
Some(name) => Some(make_kind_chunk(node, ChunkKind::Variant, Some(name), source, None)),
|
||||
None => Some(group_candidate(node, ChunkKind::Variants, source)),
|
||||
},
|
||||
|
||||
// ── Static blocks ──
|
||||
"class_static_block" => {
|
||||
Some(make_kind_chunk(node, ChunkKind::StaticInit, None, source, None))
|
||||
},
|
||||
|
||||
// ── Types ──
|
||||
"type_alias_declaration" => Some(named_candidate(node, ChunkKind::Type, source, None)),
|
||||
|
||||
_ => None,
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match context {
|
||||
ChunkContext::Root => classify_root_custom(node, source),
|
||||
ChunkContext::ClassBody => classify_class_custom(node, source),
|
||||
ChunkContext::FunctionBody => Some(classify_function_js(node, source)),
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(classify_function_js(node, source))
|
||||
}
|
||||
}
|
||||
|
||||
// ── Function body classification (JS/TS) ────────────────────────────────
|
||||
fn classify_root_custom<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// ── Exports / decorators ──
|
||||
"export_statement" => Some(classify_export_statement(node, source)),
|
||||
"decorated_definition" => Some(classify_decorated(node, source)),
|
||||
|
||||
// ── Variables ──
|
||||
"lexical_declaration" | "variable_declaration" => Some(classify_var_decl_js(node, source)),
|
||||
|
||||
// ── Containers with custom recursion ──
|
||||
"internal_module" => {
|
||||
Some(container_candidate(node, ChunkKind::Module, source, recurse_internal_module(node)))
|
||||
},
|
||||
|
||||
// ── Control flow at top level ──
|
||||
"if_statement" | "switch_statement" | "switch_expression" | "try_statement"
|
||||
| "for_statement" | "for_in_statement" | "for_of_statement" | "while_statement"
|
||||
| "do_statement" | "with_statement" => Some(classify_function_js(node, source)),
|
||||
|
||||
// ── Statements ──
|
||||
"expression_statement" => {
|
||||
// Unwrap `expression_statement` wrapping an `internal_module` (namespace).
|
||||
let inner = named_children(node)
|
||||
.into_iter()
|
||||
.find(|c| c.kind() == "internal_module");
|
||||
if let Some(ns) = inner {
|
||||
Some(container_candidate(ns, ChunkKind::Module, source, recurse_internal_module(ns)))
|
||||
} else {
|
||||
Some(group_candidate(node, ChunkKind::Statements, source))
|
||||
}
|
||||
},
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_class_custom<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// ── Exports / decorators (re-exported members) ──
|
||||
"export_statement" => Some(classify_export_statement(node, source)),
|
||||
"decorated_definition" => Some(classify_decorated(node, source)),
|
||||
|
||||
// ── Variables ──
|
||||
"lexical_declaration" | "variable_declaration" => Some(classify_var_decl_js(node, source)),
|
||||
|
||||
// ── Methods ──
|
||||
"method_definition" | "method_signature" | "abstract_method_signature" => {
|
||||
let name = extract_identifier(node, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
if name == "constructor" {
|
||||
Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Constructor,
|
||||
None,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
))
|
||||
} else {
|
||||
Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
Some(name),
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
))
|
||||
}
|
||||
},
|
||||
|
||||
// ── Fields ──
|
||||
"public_field_definition"
|
||||
| "field_definition"
|
||||
| "property_definition"
|
||||
| "property_signature"
|
||||
| "property_declaration"
|
||||
| "abstract_class_field" => match extract_identifier(node, source) {
|
||||
Some(name) => Some(make_kind_chunk(node, ChunkKind::Field, Some(name), source, None)),
|
||||
None => Some(group_candidate(node, ChunkKind::Fields, source)),
|
||||
},
|
||||
|
||||
// ── Enum members ──
|
||||
"enum_assignment" | "enum_member_declaration" => match extract_identifier(node, source) {
|
||||
Some(name) => Some(make_kind_chunk(node, ChunkKind::Variant, Some(name), source, None)),
|
||||
None => Some(group_candidate(node, ChunkKind::Variants, source)),
|
||||
},
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Classify nodes inside a function body for JS/TS.
|
||||
fn classify_function_js<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
|
||||
@@ -209,8 +254,6 @@ fn classify_function_js<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'
|
||||
}
|
||||
}
|
||||
|
||||
// ── Variable declaration (JS/TS) ────────────────────────────────────────
|
||||
|
||||
/// Classify `const`/`let`/`var` declarations, promoting arrow functions
|
||||
/// and class expressions to fn_/class_ chunks.
|
||||
fn classify_var_decl_js<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
|
||||
@@ -255,8 +298,6 @@ fn group_from_sanitized<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'
|
||||
make_candidate(node, kind, identifier, NameStyle::Group, None, None, source)
|
||||
}
|
||||
|
||||
// ── Export statement ─────────────────────────────────────────────────────
|
||||
|
||||
/// Unwrap `export` / `export default` to classify the inner declaration.
|
||||
///
|
||||
/// Named exports delegate to the appropriate container/named-chunk builder;
|
||||
|
||||
@@ -2,10 +2,47 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{
|
||||
ClassifierTables, LangClassifier, NamingMode, RecurseMode, RuleStyle, StructuralOverrides,
|
||||
semantic_rule,
|
||||
},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct JustClassifier;
|
||||
|
||||
const JUST_FUNCTION_RULES: &[super::classify::SemanticRule] = &[
|
||||
semantic_rule(
|
||||
"recipe_line",
|
||||
ChunkKind::Cmd,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"shebang",
|
||||
ChunkKind::Shebang,
|
||||
RuleStyle::Named,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
];
|
||||
|
||||
const JUST_TABLES: ClassifierTables = ClassifierTables {
|
||||
root: &[],
|
||||
class: &[],
|
||||
function: JUST_FUNCTION_RULES,
|
||||
structural_overrides: StructuralOverrides {
|
||||
extra_trivia: &[],
|
||||
preserved_trivia: &[],
|
||||
extra_root_wrappers: &["source_file"],
|
||||
preserved_root_wrappers: &[],
|
||||
absorbable_attrs: &[],
|
||||
},
|
||||
};
|
||||
|
||||
fn first_named_child(node: Node<'_>) -> Option<Node<'_>> {
|
||||
named_children(node).into_iter().next()
|
||||
}
|
||||
@@ -73,19 +110,20 @@ fn classify_just_body_node<'t>(node: Node<'t>, source: &str) -> Option<RawChunkC
|
||||
}
|
||||
|
||||
impl LangClassifier for JustClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_just_root_node(node, source)
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
&JUST_TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, _node: Node<'t>, _source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_just_body_node(node, source)
|
||||
}
|
||||
|
||||
fn is_root_wrapper(&self, kind: &str) -> bool {
|
||||
kind == "source_file"
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match context {
|
||||
ChunkContext::Root => classify_just_root_node(node, source),
|
||||
ChunkContext::FunctionBody => classify_just_body_node(node, source),
|
||||
ChunkContext::ClassBody => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{ClassifierTables, LangClassifier},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct MarkupClassifier;
|
||||
|
||||
@@ -57,36 +61,34 @@ impl MarkupClassifier {
|
||||
}
|
||||
|
||||
impl LangClassifier for MarkupClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
"section" => Some(Self::classify_section(node, source)),
|
||||
"block_statement" => Some(Self::classify_block_statement(node, source)),
|
||||
"mustache_statement" => Some(Self::classify_mustache_statement(node, source)),
|
||||
"element" | "script_element" | "style_element" | "element_node" | "text_node" => {
|
||||
Self::classify_element(node, source)
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
static TABLES: ClassifierTables = ClassifierTables {
|
||||
root: &[],
|
||||
class: &[],
|
||||
function: &[],
|
||||
structural_overrides: super::classify::StructuralOverrides::EMPTY,
|
||||
};
|
||||
&TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
"section" => Some(Self::classify_section(node, source)),
|
||||
"block_statement" => Some(Self::classify_block_statement(node, source)),
|
||||
"mustache_statement" => Some(Self::classify_mustache_statement(node, source)),
|
||||
"element" | "script_element" | "style_element" | "element_node" | "text_node" => {
|
||||
Self::classify_element(node, source)
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_function<'t>(
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
_node: Node<'t>,
|
||||
_source: &str,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
None
|
||||
if !matches!(context, ChunkContext::Root | ChunkContext::ClassBody) {
|
||||
return None;
|
||||
}
|
||||
match node.kind() {
|
||||
"section" => Some(Self::classify_section(node, source)),
|
||||
"block_statement" => Some(Self::classify_block_statement(node, source)),
|
||||
"mustache_statement" => Some(Self::classify_mustache_statement(node, source)),
|
||||
"element" | "script_element" | "style_element" | "element_node" | "text_node" => {
|
||||
Self::classify_element(node, source)
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1129
-371
File diff suppressed because it is too large
Load Diff
@@ -2,10 +2,27 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{ClassifierTables, LangClassifier, StructuralOverrides},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct NixHclClassifier;
|
||||
|
||||
const NIX_HCL_TABLES: ClassifierTables = ClassifierTables {
|
||||
root: &[],
|
||||
class: &[],
|
||||
function: &[],
|
||||
structural_overrides: StructuralOverrides {
|
||||
extra_trivia: &[],
|
||||
preserved_trivia: &[],
|
||||
extra_root_wrappers: &["body"],
|
||||
preserved_root_wrappers: &[],
|
||||
absorbable_attrs: &[],
|
||||
},
|
||||
};
|
||||
|
||||
/// Extract a structured name from an HCL `block` node.
|
||||
///
|
||||
/// Shape: `block_type label1 label2 … { body }` where labels are `string_lit`.
|
||||
@@ -77,6 +94,10 @@ fn classify_nix_binding<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'
|
||||
}
|
||||
|
||||
impl LangClassifier for NixHclClassifier {
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
&NIX_HCL_TABLES
|
||||
}
|
||||
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// Nix top-level attrsets should recurse into their binding_set so the file exposes
|
||||
|
||||
@@ -2,72 +2,95 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{ClassifierTables, LangClassifier, StructuralOverrides},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct OcamlClassifier;
|
||||
|
||||
impl LangClassifier for OcamlClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_ocaml_item(node, source)
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
static TABLES: ClassifierTables = ClassifierTables {
|
||||
root: &[],
|
||||
class: &[],
|
||||
function: &[],
|
||||
structural_overrides: StructuralOverrides::EMPTY,
|
||||
};
|
||||
&TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
"method_definition" => Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
ocaml_named_text(node, source, &["method_name"]),
|
||||
source,
|
||||
ocaml_method_recurse(node),
|
||||
)),
|
||||
"method_specification" => Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
ocaml_named_text(node, source, &["method_name"]),
|
||||
source,
|
||||
None,
|
||||
)),
|
||||
"instance_variable_definition" => {
|
||||
Some(match ocaml_named_text(node, source, &["instance_variable_name"]) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Field, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Fields, source),
|
||||
})
|
||||
},
|
||||
_ => classify_ocaml_item(node, source),
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match context {
|
||||
ChunkContext::Root => classify_ocaml_item(node, source),
|
||||
ChunkContext::ClassBody => classify_class(node, source),
|
||||
ChunkContext::FunctionBody => classify_function(node, source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
"function_expression" | "match_expression" => Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::Match,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
source,
|
||||
)),
|
||||
"match_case" => Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::Case,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
source,
|
||||
)),
|
||||
"let_expression" => Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::Let,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
source,
|
||||
)),
|
||||
_ => classify_ocaml_item(node, source),
|
||||
}
|
||||
fn classify_class<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
"method_definition" => Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
ocaml_named_text(node, source, &["method_name"]),
|
||||
source,
|
||||
ocaml_method_recurse(node),
|
||||
)),
|
||||
"method_specification" => Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
ocaml_named_text(node, source, &["method_name"]),
|
||||
source,
|
||||
None,
|
||||
)),
|
||||
"instance_variable_definition" => {
|
||||
Some(match ocaml_named_text(node, source, &["instance_variable_name"]) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Field, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Fields, source),
|
||||
})
|
||||
},
|
||||
_ => classify_ocaml_item(node, source),
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_function<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
"function_expression" | "match_expression" => Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::Match,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
source,
|
||||
)),
|
||||
"match_case" => Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::Case,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
source,
|
||||
)),
|
||||
"let_expression" => Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::Let,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
source,
|
||||
)),
|
||||
_ => classify_ocaml_item(node, source),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,21 +2,76 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{
|
||||
ClassifierTables, LangClassifier, NamingMode, RecurseMode, RuleStyle, StructuralOverrides,
|
||||
semantic_rule,
|
||||
},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct PerlClassifier;
|
||||
|
||||
const PERL_SHARED_RULES: &[super::classify::SemanticRule] = &[
|
||||
semantic_rule(
|
||||
"use_statement",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"conditional_statement",
|
||||
ChunkKind::If,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"for_statement",
|
||||
ChunkKind::Loop,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"loop_statement",
|
||||
ChunkKind::Loop,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
];
|
||||
|
||||
const PERL_TABLES: ClassifierTables = ClassifierTables {
|
||||
root: PERL_SHARED_RULES,
|
||||
class: &[],
|
||||
function: PERL_SHARED_RULES,
|
||||
structural_overrides: StructuralOverrides {
|
||||
extra_trivia: &[],
|
||||
preserved_trivia: &[],
|
||||
extra_root_wrappers: &["statement_list"],
|
||||
preserved_root_wrappers: &[],
|
||||
absorbable_attrs: &[],
|
||||
},
|
||||
};
|
||||
|
||||
impl LangClassifier for PerlClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_perl_node(node, source)
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
&PERL_TABLES
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_perl_node(node, source)
|
||||
}
|
||||
|
||||
fn is_root_wrapper(&self, kind: &str) -> bool {
|
||||
kind == "statement_list"
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match context {
|
||||
ChunkContext::Root | ChunkContext::FunctionBody => classify_perl_node(node, source),
|
||||
ChunkContext::ClassBody => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +82,6 @@ fn classify_perl_node<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandid
|
||||
"package_statement" => {
|
||||
make_kind_chunk(node, ChunkKind::Module, Some(perl_name(node, source)?), source, None)
|
||||
},
|
||||
"use_statement" => group_candidate(node, ChunkKind::Imports, source),
|
||||
"subroutine_declaration_statement" => make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
@@ -35,12 +89,6 @@ fn classify_perl_node<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandid
|
||||
source,
|
||||
body_recurse(),
|
||||
),
|
||||
"conditional_statement" => {
|
||||
make_candidate(node, ChunkKind::If, None, NameStyle::Named, None, body_recurse(), source)
|
||||
},
|
||||
"for_statement" | "loop_statement" => {
|
||||
make_candidate(node, ChunkKind::Loop, None, NameStyle::Named, None, body_recurse(), source)
|
||||
},
|
||||
"expression_statement" => classify_perl_statement(node, source),
|
||||
_ => return None,
|
||||
})
|
||||
|
||||
@@ -2,118 +2,187 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{
|
||||
ClassifierTables, LangClassifier, NamingMode, RecurseMode, RuleStyle, StructuralOverrides,
|
||||
semantic_rule,
|
||||
},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct PowershellClassifier;
|
||||
|
||||
static POWERSHELL_TABLES: ClassifierTables = ClassifierTables {
|
||||
root: &[
|
||||
semantic_rule(
|
||||
"param_block",
|
||||
ChunkKind::Parameters,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"flow_control_statement",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
],
|
||||
class: &[],
|
||||
function: &[
|
||||
semantic_rule(
|
||||
"class_method_parameter_list",
|
||||
ChunkKind::Parameters,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"param_block",
|
||||
ChunkKind::Parameters,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"flow_control_statement",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
],
|
||||
structural_overrides: StructuralOverrides {
|
||||
extra_trivia: &[
|
||||
"function_name",
|
||||
"simple_name",
|
||||
"type_literal",
|
||||
"switch_condition",
|
||||
],
|
||||
preserved_trivia: &[],
|
||||
extra_root_wrappers: &[],
|
||||
preserved_root_wrappers: &[],
|
||||
absorbable_attrs: &[],
|
||||
},
|
||||
};
|
||||
|
||||
impl LangClassifier for PowershellClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
"param_block" => group_candidate(node, ChunkKind::Parameters, source),
|
||||
"statement_list" => make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Body,
|
||||
None,
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::Root)),
|
||||
),
|
||||
"class_statement" => make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Class,
|
||||
Some(powershell_name(node, source)?),
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::ClassBody)),
|
||||
),
|
||||
"function_statement" => make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
Some(powershell_name(node, source)?),
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
),
|
||||
"pipeline" => classify_powershell_pipeline(node, source),
|
||||
"switch_statement" | "if_statement" | "foreach_statement" => {
|
||||
return self.classify_function(node, source);
|
||||
},
|
||||
"flow_control_statement" => group_candidate(node, ChunkKind::Statements, source),
|
||||
_ => return None,
|
||||
})
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
&POWERSHELL_TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
"class_property_definition" => match powershell_name(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Field, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Fields, source),
|
||||
},
|
||||
"class_method_definition" => classify_class_method(node, source)?,
|
||||
_ => return None,
|
||||
})
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match context {
|
||||
ChunkContext::Root => classify_root_custom(node, source),
|
||||
ChunkContext::ClassBody => classify_class_custom(node, source),
|
||||
ChunkContext::FunctionBody => classify_function_custom(node, source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
"class_method_parameter_list" | "param_block" => {
|
||||
group_candidate(node, ChunkKind::Parameters, source)
|
||||
},
|
||||
"script_block" => make_container_chunk(
|
||||
node,
|
||||
block_kind_for_parent(node),
|
||||
None,
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
),
|
||||
"script_block_body" | "statement_block" => make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Block,
|
||||
None,
|
||||
source,
|
||||
recurse_into(node, ChunkContext::FunctionBody, &[], &["statement_list"]),
|
||||
),
|
||||
"pipeline" => classify_powershell_pipeline(node, source),
|
||||
"if_statement" => make_container_chunk(
|
||||
node,
|
||||
ChunkKind::If,
|
||||
None,
|
||||
source,
|
||||
recurse_into(node, ChunkContext::FunctionBody, &[], &["statement_block"]),
|
||||
),
|
||||
"foreach_statement" => make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Loop,
|
||||
None,
|
||||
source,
|
||||
recurse_into(node, ChunkContext::FunctionBody, &[], &["statement_block"]),
|
||||
),
|
||||
"switch_statement" => make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Switch,
|
||||
None,
|
||||
source,
|
||||
recurse_into(node, ChunkContext::FunctionBody, &[], &["switch_body"]),
|
||||
),
|
||||
"switch_clauses" => make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Cases,
|
||||
None,
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
),
|
||||
"switch_clause" => make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Case,
|
||||
None,
|
||||
source,
|
||||
recurse_into(node, ChunkContext::FunctionBody, &[], &["statement_block"]),
|
||||
),
|
||||
"flow_control_statement" => group_candidate(node, ChunkKind::Statements, source),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
fn classify_root_custom<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
"statement_list" => make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Body,
|
||||
None,
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::Root)),
|
||||
),
|
||||
"class_statement" => make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Class,
|
||||
Some(powershell_name(node, source)?),
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::ClassBody)),
|
||||
),
|
||||
"function_statement" => make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
Some(powershell_name(node, source)?),
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
),
|
||||
"pipeline" => classify_powershell_pipeline(node, source),
|
||||
"switch_statement" | "if_statement" | "foreach_statement" => {
|
||||
return classify_function_custom(node, source);
|
||||
},
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn is_trivia(&self, kind: &str) -> bool {
|
||||
matches!(kind, "function_name" | "simple_name" | "type_literal" | "switch_condition")
|
||||
}
|
||||
fn classify_class_custom<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
"class_property_definition" => match powershell_name(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Field, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Fields, source),
|
||||
},
|
||||
"class_method_definition" => classify_class_method(node, source)?,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn classify_function_custom<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
"script_block" => make_container_chunk(
|
||||
node,
|
||||
block_kind_for_parent(node),
|
||||
None,
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
),
|
||||
"script_block_body" | "statement_block" => make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Block,
|
||||
None,
|
||||
source,
|
||||
recurse_into(node, ChunkContext::FunctionBody, &[], &["statement_list"]),
|
||||
),
|
||||
"pipeline" => classify_powershell_pipeline(node, source),
|
||||
"if_statement" => make_container_chunk(
|
||||
node,
|
||||
ChunkKind::If,
|
||||
None,
|
||||
source,
|
||||
recurse_into(node, ChunkContext::FunctionBody, &[], &["statement_block"]),
|
||||
),
|
||||
"foreach_statement" => make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Loop,
|
||||
None,
|
||||
source,
|
||||
recurse_into(node, ChunkContext::FunctionBody, &[], &["statement_block"]),
|
||||
),
|
||||
"switch_statement" => make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Switch,
|
||||
None,
|
||||
source,
|
||||
recurse_into(node, ChunkContext::FunctionBody, &[], &["switch_body"]),
|
||||
),
|
||||
"switch_clauses" => make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Cases,
|
||||
None,
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::FunctionBody)),
|
||||
),
|
||||
"switch_clause" => make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Case,
|
||||
None,
|
||||
source,
|
||||
recurse_into(node, ChunkContext::FunctionBody, &[], &["statement_block"]),
|
||||
),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn classify_class_method<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
|
||||
@@ -7,25 +7,83 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{
|
||||
ClassifierTables, LangClassifier, NamingMode, RecurseMode, RuleStyle, semantic_rule,
|
||||
},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct ProtoClassifier;
|
||||
|
||||
const PROTO_ROOT_RULES: &[super::classify::SemanticRule] = &[
|
||||
semantic_rule(
|
||||
"syntax",
|
||||
ChunkKind::Headers,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"package",
|
||||
ChunkKind::Headers,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"import",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"option",
|
||||
ChunkKind::Options,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
];
|
||||
|
||||
const PROTO_CLASS_RULES: &[super::classify::SemanticRule] = &[semantic_rule(
|
||||
"option",
|
||||
ChunkKind::Options,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
)];
|
||||
|
||||
const PROTO_TABLES: ClassifierTables = ClassifierTables {
|
||||
root: PROTO_ROOT_RULES,
|
||||
class: PROTO_CLASS_RULES,
|
||||
function: &[],
|
||||
structural_overrides: super::classify::StructuralOverrides::EMPTY,
|
||||
};
|
||||
|
||||
impl LangClassifier for ProtoClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_proto_root(node, source)
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
&PROTO_TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_proto_class(node, source)
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match context {
|
||||
ChunkContext::Root => classify_proto_root(node, source),
|
||||
ChunkContext::ClassBody => classify_proto_class(node, source),
|
||||
ChunkContext::FunctionBody => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_proto_root<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
"syntax" | "package" => group_candidate(node, ChunkKind::Headers, source),
|
||||
"import" => group_candidate(node, ChunkKind::Imports, source),
|
||||
"option" => group_candidate(node, ChunkKind::Options, source),
|
||||
"message" => make_named_proto_chunk(
|
||||
node,
|
||||
ChunkKind::Type,
|
||||
@@ -53,7 +111,6 @@ fn classify_proto_root<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandi
|
||||
|
||||
fn classify_proto_class<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
"option" => group_candidate(node, ChunkKind::Options, source),
|
||||
"field" if is_proto_message_field(node) => {
|
||||
make_kind_chunk(node, ChunkKind::Field, proto_name(node, source), source, None)
|
||||
},
|
||||
|
||||
@@ -2,217 +2,286 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{
|
||||
ClassifierTables, LangClassifier, NamingMode, RecurseMode, RuleStyle, semantic_rule,
|
||||
},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct PythonClassifier;
|
||||
|
||||
const ROOT_RULES: &[super::classify::SemanticRule] = &[
|
||||
semantic_rule(
|
||||
"import_statement",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"import_from_statement",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"assignment",
|
||||
ChunkKind::Declarations,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"function_definition",
|
||||
ChunkKind::Function,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"class_definition",
|
||||
ChunkKind::Class,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"if_statement",
|
||||
ChunkKind::If,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"for_statement",
|
||||
ChunkKind::Loop,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"while_statement",
|
||||
ChunkKind::Loop,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"try_statement",
|
||||
ChunkKind::Try,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"with_statement",
|
||||
ChunkKind::Block,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"expression_statement",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"global_statement",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
];
|
||||
|
||||
const CLASS_RULES: &[super::classify::SemanticRule] = &[
|
||||
semantic_rule(
|
||||
"expression_statement",
|
||||
ChunkKind::Fields,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"assignment",
|
||||
ChunkKind::Fields,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"type_alias_statement",
|
||||
ChunkKind::Type,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::None,
|
||||
),
|
||||
];
|
||||
|
||||
const FUNCTION_RULES: &[super::classify::SemanticRule] = &[
|
||||
semantic_rule(
|
||||
"if_statement",
|
||||
ChunkKind::If,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"for_statement",
|
||||
ChunkKind::Loop,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"while_statement",
|
||||
ChunkKind::Loop,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"try_statement",
|
||||
ChunkKind::Try,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"with_statement",
|
||||
ChunkKind::Block,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"elif_clause",
|
||||
ChunkKind::Elif,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"except_clause",
|
||||
ChunkKind::Except,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"match_statement",
|
||||
ChunkKind::Match,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
];
|
||||
|
||||
const PYTHON_TABLES: ClassifierTables = ClassifierTables {
|
||||
root: ROOT_RULES,
|
||||
class: CLASS_RULES,
|
||||
function: FUNCTION_RULES,
|
||||
structural_overrides: super::classify::StructuralOverrides::EMPTY,
|
||||
};
|
||||
|
||||
impl LangClassifier for PythonClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// ── Imports ──
|
||||
"import_statement" | "import_from_statement" => {
|
||||
Some(group_candidate(node, ChunkKind::Imports, source))
|
||||
},
|
||||
|
||||
// ── Variables / assignments ──
|
||||
"assignment" => Some(group_candidate(node, ChunkKind::Declarations, source)),
|
||||
|
||||
// ── Functions ──
|
||||
"function_definition" => Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
extract_identifier(node, source),
|
||||
source,
|
||||
recurse_into(node, ChunkContext::FunctionBody, &["body"], &["block"]),
|
||||
)),
|
||||
|
||||
// ── Containers ──
|
||||
"class_definition" => Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Class,
|
||||
extract_identifier(node, source),
|
||||
source,
|
||||
recurse_into(node, ChunkContext::ClassBody, &["body"], &["block"]),
|
||||
)),
|
||||
|
||||
// ── Control flow (top-level scripts) ──
|
||||
"if_statement" | "for_statement" | "while_statement" | "try_statement"
|
||||
| "with_statement" => Some(classify_function_python(node, source)),
|
||||
|
||||
// ── Statements ──
|
||||
"expression_statement" | "global_statement" => {
|
||||
Some(group_candidate(node, ChunkKind::Statements, source))
|
||||
},
|
||||
|
||||
// ── Decorated ──
|
||||
"decorated_definition" => Some(classify_decorated(node, source)),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
&PYTHON_TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// ── Methods ──
|
||||
"function_definition" => {
|
||||
let name = extract_identifier(node, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
let kind = if name == "__init__" || name == "__new__" {
|
||||
ChunkKind::Constructor
|
||||
} else {
|
||||
ChunkKind::Function
|
||||
};
|
||||
let identifier = if kind == ChunkKind::Constructor {
|
||||
None
|
||||
} else {
|
||||
Some(name)
|
||||
};
|
||||
Some(make_kind_chunk(
|
||||
node,
|
||||
kind,
|
||||
identifier,
|
||||
source,
|
||||
recurse_into(node, ChunkContext::FunctionBody, &["body"], &["block"]),
|
||||
))
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match context {
|
||||
ChunkContext::Root | ChunkContext::ClassBody if node.kind() == "decorated_definition" => {
|
||||
Some(classify_decorated(node, source, context))
|
||||
},
|
||||
|
||||
// ── Decorated methods ──
|
||||
"decorated_definition" => {
|
||||
let inner = named_children(node)
|
||||
.into_iter()
|
||||
.find(|c| c.kind() == "function_definition");
|
||||
if let Some(child) = inner {
|
||||
let name =
|
||||
extract_identifier(child, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
let kind = if name == "__init__" || name == "__new__" {
|
||||
ChunkKind::Constructor
|
||||
} else {
|
||||
ChunkKind::Function
|
||||
};
|
||||
let identifier = if kind == ChunkKind::Constructor {
|
||||
None
|
||||
} else {
|
||||
Some(name)
|
||||
};
|
||||
Some(make_kind_chunk(
|
||||
node,
|
||||
kind,
|
||||
identifier,
|
||||
source,
|
||||
recurse_into(child, ChunkContext::FunctionBody, &["body"], &["block"]),
|
||||
))
|
||||
} else {
|
||||
Some(infer_named_candidate(node, source))
|
||||
}
|
||||
ChunkContext::ClassBody if node.kind() == "function_definition" => {
|
||||
Some(classify_class_method(node, source))
|
||||
},
|
||||
|
||||
// ── Fields ──
|
||||
"expression_statement" | "assignment" => {
|
||||
Some(group_candidate(node, ChunkKind::Fields, source))
|
||||
},
|
||||
|
||||
// ── Type aliases ──
|
||||
"type_alias_statement" => Some(named_candidate(node, ChunkKind::Type, source, None)),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// ── Control flow ──
|
||||
"if_statement" => Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::If,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
None,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
source,
|
||||
)),
|
||||
"for_statement" | "while_statement" => Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::Loop,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
None,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
source,
|
||||
)),
|
||||
"try_statement" => Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::Try,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
None,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
source,
|
||||
)),
|
||||
"with_statement" => Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::Block,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
None,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
source,
|
||||
)),
|
||||
|
||||
// ── Positional ──
|
||||
"elif_clause" => Some(positional_candidate(node, ChunkKind::Elif, source)),
|
||||
"except_clause" => Some(positional_candidate(node, ChunkKind::Except, source)),
|
||||
"match_statement" => Some(positional_candidate(node, ChunkKind::Match, source)),
|
||||
|
||||
// ── Variables / simple statements ──
|
||||
// Catch `expression_statement`, `assignment`, `return_statement`,
|
||||
// `raise_statement`, `pass_statement`, `break_statement`,
|
||||
// `continue_statement`, `delete_statement`, `assert_statement`,
|
||||
// `nonlocal_statement`, `global_statement`, `type_alias_statement`,
|
||||
// and any other leaf statements so they merge into the parent
|
||||
// function body instead of becoming standalone addressable chunks.
|
||||
_ => Some(group_candidate(node, ChunkKind::Statements, source)),
|
||||
}
|
||||
let _ = source;
|
||||
Some(group_candidate(node, ChunkKind::Statements, source))
|
||||
}
|
||||
}
|
||||
|
||||
/// Classify Python function-level nodes (reused for top-level control flow
|
||||
/// delegation).
|
||||
fn classify_function_python<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
|
||||
let fn_recurse = || recurse_body(node, ChunkContext::FunctionBody);
|
||||
match node.kind() {
|
||||
"if_statement" => {
|
||||
make_candidate(node, ChunkKind::If, None, NameStyle::Named, None, fn_recurse(), source)
|
||||
},
|
||||
"for_statement" | "while_statement" => {
|
||||
make_candidate(node, ChunkKind::Loop, None, NameStyle::Named, None, fn_recurse(), source)
|
||||
},
|
||||
"try_statement" => {
|
||||
make_candidate(node, ChunkKind::Try, None, NameStyle::Named, None, fn_recurse(), source)
|
||||
},
|
||||
"with_statement" => {
|
||||
make_candidate(node, ChunkKind::Block, None, NameStyle::Named, None, fn_recurse(), source)
|
||||
},
|
||||
_ => group_candidate(node, ChunkKind::Statements, source),
|
||||
}
|
||||
fn classify_class_method<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
|
||||
let name = extract_identifier(node, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
let kind = if name == "__init__" || name == "__new__" {
|
||||
ChunkKind::Constructor
|
||||
} else {
|
||||
ChunkKind::Function
|
||||
};
|
||||
let identifier = if kind == ChunkKind::Constructor {
|
||||
None
|
||||
} else {
|
||||
Some(name)
|
||||
};
|
||||
make_kind_chunk(
|
||||
node,
|
||||
kind,
|
||||
identifier,
|
||||
source,
|
||||
resolve_recurse(node, ChunkContext::FunctionBody),
|
||||
)
|
||||
}
|
||||
|
||||
fn classify_decorated<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
|
||||
fn classify_decorated<'t>(
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
context: ChunkContext,
|
||||
) -> RawChunkCandidate<'t> {
|
||||
let inner = named_children(node)
|
||||
.into_iter()
|
||||
.find(|c| c.kind() == "class_definition" || c.kind() == "function_definition");
|
||||
.find(|c| matches!(c.kind(), "class_definition" | "function_definition"));
|
||||
match inner {
|
||||
Some(child) if child.kind() == "class_definition" => make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Class,
|
||||
extract_identifier(child, source),
|
||||
source,
|
||||
recurse_into(child, ChunkContext::ClassBody, &["body"], &["block"]),
|
||||
resolve_recurse(child, ChunkContext::ClassBody),
|
||||
),
|
||||
Some(child)
|
||||
if child.kind() == "function_definition" && context == ChunkContext::ClassBody =>
|
||||
{
|
||||
let name = extract_identifier(child, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
let kind = if name == "__init__" || name == "__new__" {
|
||||
ChunkKind::Constructor
|
||||
} else {
|
||||
ChunkKind::Function
|
||||
};
|
||||
let identifier = if kind == ChunkKind::Constructor {
|
||||
None
|
||||
} else {
|
||||
Some(name)
|
||||
};
|
||||
make_kind_chunk(
|
||||
node,
|
||||
kind,
|
||||
identifier,
|
||||
source,
|
||||
resolve_recurse(child, ChunkContext::FunctionBody),
|
||||
)
|
||||
},
|
||||
Some(child) if child.kind() == "function_definition" => make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
extract_identifier(child, source),
|
||||
source,
|
||||
recurse_into(child, ChunkContext::FunctionBody, &["body"], &["block"]),
|
||||
resolve_recurse(child, ChunkContext::FunctionBody),
|
||||
),
|
||||
_ => positional_candidate(node, ChunkKind::Block, source),
|
||||
}
|
||||
|
||||
@@ -2,57 +2,83 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{ClassifierTables, LangClassifier},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct RClassifier;
|
||||
|
||||
static R_TABLES: ClassifierTables = ClassifierTables {
|
||||
root: &[],
|
||||
class: &[],
|
||||
function: &[],
|
||||
structural_overrides: super::classify::StructuralOverrides::EMPTY,
|
||||
};
|
||||
|
||||
impl LangClassifier for RClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
// ── Imports ──
|
||||
"call" if is_import_call(node, source) => {
|
||||
group_candidate(node, ChunkKind::Imports, source)
|
||||
},
|
||||
"call" => group_candidate(node, ChunkKind::Statements, source),
|
||||
|
||||
// ── Function / value assignments ──
|
||||
"binary_operator" => classify_assignment(node, source, ChunkScope::Root)?,
|
||||
|
||||
// ── Control flow at script scope ──
|
||||
"if_statement" => control_candidate(node, ChunkKind::If, source, recurse_if(node)),
|
||||
"for_statement" | "while_statement" | "repeat_statement" => {
|
||||
control_candidate(node, ChunkKind::Loop, source, recurse_loop(node))
|
||||
},
|
||||
|
||||
// ── Bare expressions ──
|
||||
"identifier" | "subset" | "subset2" | "extract_operator" => {
|
||||
group_candidate(node, ChunkKind::Statements, source)
|
||||
},
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
&R_TABLES
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
// ── Local assignments ──
|
||||
"binary_operator" => classify_assignment(node, source, ChunkScope::Function)?,
|
||||
|
||||
// ── Control flow ──
|
||||
"if_statement" => control_candidate(node, ChunkKind::If, source, recurse_if(node)),
|
||||
"for_statement" | "while_statement" | "repeat_statement" => {
|
||||
control_candidate(node, ChunkKind::Loop, source, recurse_loop(node))
|
||||
},
|
||||
|
||||
// ── Calls / bare expressions ──
|
||||
"call" | "identifier" | "subset" | "subset2" | "extract_operator" | "break" | "next"
|
||||
| "return" => group_candidate(node, ChunkKind::Statements, source),
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match context {
|
||||
ChunkContext::Root => classify_root_custom(node, source),
|
||||
ChunkContext::FunctionBody => classify_function_custom(node, source),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_root_custom<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
// ── Imports ──
|
||||
"call" if is_import_call(node, source) => group_candidate(node, ChunkKind::Imports, source),
|
||||
"call" => group_candidate(node, ChunkKind::Statements, source),
|
||||
|
||||
// ── Function / value assignments ──
|
||||
"binary_operator" => classify_assignment(node, source, ChunkScope::Root)?,
|
||||
|
||||
// ── Control flow at script scope ──
|
||||
"if_statement" => control_candidate(node, ChunkKind::If, source, recurse_if(node)),
|
||||
"for_statement" | "while_statement" | "repeat_statement" => {
|
||||
control_candidate(node, ChunkKind::Loop, source, recurse_loop(node))
|
||||
},
|
||||
|
||||
// ── Bare expressions ──
|
||||
"identifier" | "subset" | "subset2" | "extract_operator" => {
|
||||
group_candidate(node, ChunkKind::Statements, source)
|
||||
},
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn classify_function_custom<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
// ── Local assignments ──
|
||||
"binary_operator" => classify_assignment(node, source, ChunkScope::Function)?,
|
||||
|
||||
// ── Control flow ──
|
||||
"if_statement" => control_candidate(node, ChunkKind::If, source, recurse_if(node)),
|
||||
"for_statement" | "while_statement" | "repeat_statement" => {
|
||||
control_candidate(node, ChunkKind::Loop, source, recurse_loop(node))
|
||||
},
|
||||
|
||||
// ── Calls / bare expressions ──
|
||||
"call" | "identifier" | "subset" | "subset2" | "extract_operator" | "break" | "next"
|
||||
| "return" => group_candidate(node, ChunkKind::Statements, source),
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ChunkScope {
|
||||
Root,
|
||||
|
||||
@@ -2,137 +2,252 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{
|
||||
ClassifierTables, LangClassifier, NamingMode, RecurseMode, RuleStyle, StructuralOverrides,
|
||||
semantic_rule,
|
||||
},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct RubyLuaClassifier;
|
||||
|
||||
const RUBY_LUA_ROOT_RULES: &[super::classify::SemanticRule] = &[
|
||||
semantic_rule(
|
||||
"function_definition",
|
||||
ChunkKind::Function,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"method",
|
||||
ChunkKind::Function,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"singleton_method",
|
||||
ChunkKind::Function,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"class",
|
||||
ChunkKind::Class,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"module",
|
||||
ChunkKind::Module,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"if_statement",
|
||||
ChunkKind::If,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"unless",
|
||||
ChunkKind::If,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"while_statement",
|
||||
ChunkKind::Loop,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"for_statement",
|
||||
ChunkKind::Loop,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"assignment",
|
||||
ChunkKind::Declarations,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"expression_statement",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"function_call",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
];
|
||||
|
||||
const RUBY_LUA_CLASS_RULES: &[super::classify::SemanticRule] = &[
|
||||
semantic_rule(
|
||||
"class",
|
||||
ChunkKind::Class,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"module",
|
||||
ChunkKind::Module,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"assignment",
|
||||
ChunkKind::Fields,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"call",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"command",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"identifier",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
];
|
||||
|
||||
const RUBY_LUA_FUNCTION_RULES: &[super::classify::SemanticRule] = &[
|
||||
semantic_rule(
|
||||
"if_statement",
|
||||
ChunkKind::If,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"unless",
|
||||
ChunkKind::If,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"case_statement",
|
||||
ChunkKind::Switch,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"case_match",
|
||||
ChunkKind::Switch,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"while_statement",
|
||||
ChunkKind::Loop,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"for_statement",
|
||||
ChunkKind::Loop,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"assignment",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
];
|
||||
|
||||
const RUBY_LUA_TABLES: ClassifierTables = ClassifierTables {
|
||||
root: RUBY_LUA_ROOT_RULES,
|
||||
class: RUBY_LUA_CLASS_RULES,
|
||||
function: RUBY_LUA_FUNCTION_RULES,
|
||||
structural_overrides: StructuralOverrides {
|
||||
extra_trivia: &[],
|
||||
preserved_trivia: &[],
|
||||
extra_root_wrappers: &[],
|
||||
preserved_root_wrappers: &["module"],
|
||||
absorbable_attrs: &[],
|
||||
},
|
||||
};
|
||||
|
||||
impl LangClassifier for RubyLuaClassifier {
|
||||
fn preserve_root_wrapper(&self, kind: &str) -> bool {
|
||||
kind == "module"
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
&RUBY_LUA_TABLES
|
||||
}
|
||||
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
// ── Imports ──
|
||||
"command" | "call" => {
|
||||
// Ruby `require`/`require_relative` appear as call/command nodes.
|
||||
// Check if the target is an import keyword.
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match (context, node.kind()) {
|
||||
(ChunkContext::Root, "command" | "call") => {
|
||||
let target = extract_identifier(node, source);
|
||||
match target.as_deref() {
|
||||
Some(match target.as_deref() {
|
||||
Some("require" | "require_relative" | "load" | "autoload") => {
|
||||
group_candidate(node, ChunkKind::Imports, source)
|
||||
},
|
||||
_ => group_candidate(node, ChunkKind::Statements, source),
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// ── Functions ──
|
||||
"function_definition" => named_candidate(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
),
|
||||
"method" | "singleton_method" => named_candidate(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
),
|
||||
|
||||
// ── Containers ──
|
||||
"class" => container_candidate(node, ChunkKind::Class, source, recurse_class(node)),
|
||||
"module" => container_candidate(node, ChunkKind::Module, source, recurse_class(node)),
|
||||
|
||||
// ── Control flow (top-level scripts) ──
|
||||
"if_statement" | "unless" | "while_statement" | "for_statement" => {
|
||||
return Some(
|
||||
self
|
||||
.classify_function(node, source)
|
||||
.unwrap_or_else(|| group_candidate(node, ChunkKind::Statements, source)),
|
||||
);
|
||||
},
|
||||
|
||||
// ── Assignments ──
|
||||
"assignment" => group_candidate(node, ChunkKind::Declarations, source),
|
||||
|
||||
// ── Statements ──
|
||||
"expression_statement" | "function_call" => {
|
||||
group_candidate(node, ChunkKind::Statements, source)
|
||||
},
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
// ── Methods ──
|
||||
"method" | "singleton_method" => {
|
||||
(ChunkContext::ClassBody, "method" | "singleton_method") => {
|
||||
let name = extract_identifier(node, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
if name == "initialize" {
|
||||
make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Constructor,
|
||||
None,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)
|
||||
let kind = if name == "initialize" {
|
||||
ChunkKind::Constructor
|
||||
} else {
|
||||
make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
Some(name),
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)
|
||||
}
|
||||
ChunkKind::Function
|
||||
};
|
||||
let identifier = (kind != ChunkKind::Constructor).then_some(name);
|
||||
Some(make_kind_chunk(
|
||||
node,
|
||||
kind,
|
||||
identifier,
|
||||
source,
|
||||
resolve_recurse(node, ChunkContext::FunctionBody),
|
||||
))
|
||||
},
|
||||
|
||||
// ── Nested containers ──
|
||||
"class" => container_candidate(node, ChunkKind::Class, source, recurse_class(node)),
|
||||
"module" => container_candidate(node, ChunkKind::Module, source, recurse_class(node)),
|
||||
|
||||
// ── Fields / constants ──
|
||||
"assignment" => group_candidate(node, ChunkKind::Fields, source),
|
||||
|
||||
// ── Calls (include, attr_reader, etc.) and bare identifiers (private) ──
|
||||
"call" | "command" | "identifier" => group_candidate(node, ChunkKind::Statements, source),
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
let fn_recurse = || recurse_body(node, ChunkContext::FunctionBody);
|
||||
Some(match node.kind() {
|
||||
// ── Control flow ──
|
||||
"if_statement" | "unless" => {
|
||||
make_candidate(node, ChunkKind::If, None, NameStyle::Named, None, fn_recurse(), source)
|
||||
},
|
||||
"case_statement" | "case_match" => make_candidate(
|
||||
node,
|
||||
ChunkKind::Switch,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
None,
|
||||
fn_recurse(),
|
||||
source,
|
||||
),
|
||||
"while_statement" | "for_statement" => make_candidate(
|
||||
node,
|
||||
ChunkKind::Loop,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
None,
|
||||
fn_recurse(),
|
||||
source,
|
||||
),
|
||||
|
||||
// ── Variables ──
|
||||
"assignment" => group_candidate(node, ChunkKind::Statements, source),
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,157 +2,338 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{
|
||||
ClassifierTables, LangClassifier, NamingMode, RecurseMode, RuleStyle, StructuralOverrides,
|
||||
semantic_rule,
|
||||
},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct RustClassifier;
|
||||
|
||||
const ROOT_RULES: &[super::classify::SemanticRule] = &[
|
||||
// ── Imports ──
|
||||
semantic_rule(
|
||||
"use_declaration",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"extern_crate_declaration",
|
||||
ChunkKind::Imports,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
// ── Functions ──
|
||||
semantic_rule(
|
||||
"function_item",
|
||||
ChunkKind::Function,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"function_definition",
|
||||
ChunkKind::Function,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
// ── Containers ──
|
||||
semantic_rule(
|
||||
"struct_item",
|
||||
ChunkKind::Struct,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"enum_item",
|
||||
ChunkKind::Enum,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"trait_item",
|
||||
ChunkKind::Trait,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"mod_item",
|
||||
ChunkKind::Module,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"foreign_block",
|
||||
ChunkKind::Module,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
// ── Types ──
|
||||
semantic_rule(
|
||||
"type_item",
|
||||
ChunkKind::Type,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::ClassBody),
|
||||
),
|
||||
// ── Macros ──
|
||||
semantic_rule(
|
||||
"macro_definition",
|
||||
ChunkKind::Macro,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"macro_rule",
|
||||
ChunkKind::Macro,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
// ── Statics / consts ──
|
||||
semantic_rule(
|
||||
"static_item",
|
||||
ChunkKind::Declarations,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"const_item",
|
||||
ChunkKind::Declarations,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
// ── Attributes ──
|
||||
semantic_rule(
|
||||
"inner_attribute_item",
|
||||
ChunkKind::Attrs,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
// ── Expression statements ──
|
||||
semantic_rule(
|
||||
"expression_statement",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
];
|
||||
|
||||
const CLASS_RULES: &[super::classify::SemanticRule] = &[
|
||||
// ── Functions (methods in impl/trait) ──
|
||||
semantic_rule(
|
||||
"function_item",
|
||||
ChunkKind::Function,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
semantic_rule(
|
||||
"function_definition",
|
||||
ChunkKind::Function,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::Auto(ChunkContext::FunctionBody),
|
||||
),
|
||||
// ── Types ──
|
||||
semantic_rule(
|
||||
"type_item",
|
||||
ChunkKind::Type,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"type_alias",
|
||||
ChunkKind::Type,
|
||||
RuleStyle::Named,
|
||||
NamingMode::AutoIdentifier,
|
||||
RecurseMode::None,
|
||||
),
|
||||
// ── Consts / macros in class body ──
|
||||
semantic_rule(
|
||||
"const_item",
|
||||
ChunkKind::Fields,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"macro_invocation",
|
||||
ChunkKind::Fields,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
];
|
||||
|
||||
const FUNCTION_RULES: &[super::classify::SemanticRule] = &[
|
||||
// ── Control flow ──
|
||||
semantic_rule(
|
||||
"match_expression",
|
||||
ChunkKind::Match,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"loop_expression",
|
||||
ChunkKind::Loop,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"while_expression",
|
||||
ChunkKind::Loop,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"for_expression",
|
||||
ChunkKind::Loop,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
// ── Expression statements ──
|
||||
semantic_rule(
|
||||
"expression_statement",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
];
|
||||
|
||||
const RUST_TABLES: ClassifierTables = ClassifierTables {
|
||||
root: ROOT_RULES,
|
||||
class: CLASS_RULES,
|
||||
function: FUNCTION_RULES,
|
||||
structural_overrides: StructuralOverrides::EMPTY,
|
||||
};
|
||||
|
||||
impl LangClassifier for RustClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
// ── Imports ──
|
||||
"use_declaration" | "extern_crate_declaration" => {
|
||||
group_candidate(node, ChunkKind::Imports, source)
|
||||
},
|
||||
|
||||
// ── Functions ──
|
||||
"function_item" | "function_definition" => named_candidate(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody)
|
||||
.or_else(|| recurse_into(node, ChunkContext::FunctionBody, &["body"], &["block"])),
|
||||
),
|
||||
|
||||
// ── Containers ──
|
||||
"struct_item" => container_candidate(node, ChunkKind::Struct, source, recurse_class(node)),
|
||||
"enum_item" => container_candidate(node, ChunkKind::Enum, source, recurse_enum(node)),
|
||||
"trait_item" => container_candidate(node, ChunkKind::Trait, source, recurse_class(node)),
|
||||
"mod_item" | "foreign_block" => {
|
||||
container_candidate(node, ChunkKind::Module, source, recurse_class(node))
|
||||
},
|
||||
"impl_item" => {
|
||||
let name = extract_impl_name(node, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Impl,
|
||||
Some(name),
|
||||
source,
|
||||
recurse_into(node, ChunkContext::ClassBody, &["body"], &["declaration_list"]),
|
||||
)
|
||||
},
|
||||
|
||||
// ── Types ──
|
||||
"type_item" => named_candidate(node, ChunkKind::Type, source, recurse_class(node)),
|
||||
|
||||
// ── Macros ──
|
||||
"macro_definition" | "macro_rule" => named_candidate(
|
||||
node,
|
||||
ChunkKind::Macro,
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
),
|
||||
|
||||
// ── Statics / consts ──
|
||||
"static_item" | "const_item" => group_candidate(node, ChunkKind::Declarations, source),
|
||||
|
||||
// ── Attributes ──
|
||||
"inner_attribute_item" => group_candidate(node, ChunkKind::Attrs, source),
|
||||
|
||||
// ── Variables ──
|
||||
"let_declaration" => match extract_identifier(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Declarations, source),
|
||||
},
|
||||
|
||||
// ── Expression statements ──
|
||||
"expression_statement" => group_candidate(node, ChunkKind::Statements, source),
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
&RUST_TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
Some(match node.kind() {
|
||||
// ── Methods ──
|
||||
"function_item" | "function_definition" => {
|
||||
let name = extract_identifier(node, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Function,
|
||||
Some(name),
|
||||
source,
|
||||
recurse_body(node, ChunkContext::FunctionBody),
|
||||
)
|
||||
},
|
||||
|
||||
// ── Types ──
|
||||
"type_item" | "type_alias" => named_candidate(node, ChunkKind::Type, source, None),
|
||||
|
||||
// ── Fields ──
|
||||
"field_declaration" => match extract_identifier(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Field, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Fields, source),
|
||||
},
|
||||
|
||||
// ── Enum variants ──
|
||||
"enum_variant" => match extract_identifier(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Variant, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Variants, source),
|
||||
},
|
||||
|
||||
// ── Consts / macros in class body ──
|
||||
"const_item" | "macro_invocation" => group_candidate(node, ChunkKind::Fields, source),
|
||||
|
||||
// ── Attributes (absorbed by the framework, but handle explicitly) ──
|
||||
"attribute_item" => return None,
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match context {
|
||||
ChunkContext::Root => classify_root_custom(node, source),
|
||||
ChunkContext::ClassBody => classify_class_custom(node, source),
|
||||
ChunkContext::FunctionBody => classify_function_custom(node, source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
let fn_recurse = || recurse_body(node, ChunkContext::FunctionBody);
|
||||
Some(match node.kind() {
|
||||
// ── Control flow ──
|
||||
"if_expression" => {
|
||||
make_candidate(node, ChunkKind::If, None, NameStyle::Named, None, fn_recurse(), source)
|
||||
},
|
||||
"match_expression" => positional_candidate(node, ChunkKind::Match, source),
|
||||
"loop_expression" | "while_expression" | "for_expression" => {
|
||||
positional_candidate(node, ChunkKind::Loop, source)
|
||||
},
|
||||
|
||||
// ── Blocks ──
|
||||
"unsafe_block" | "async_block" | "const_block" | "block_expression" => make_candidate(
|
||||
fn classify_root_custom<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// ── Impl blocks (custom name extraction) ──
|
||||
"impl_item" => {
|
||||
let name = extract_impl_name(node, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Block,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
None,
|
||||
fn_recurse(),
|
||||
ChunkKind::Impl,
|
||||
Some(name),
|
||||
source,
|
||||
),
|
||||
recurse_into(node, ChunkContext::ClassBody, &["body"], &["declaration_list"]),
|
||||
))
|
||||
},
|
||||
|
||||
// ── Variables ──
|
||||
"let_declaration" => {
|
||||
let span = line_span(node.start_position().row + 1, node.end_position().row + 1);
|
||||
if span > 1 {
|
||||
match extract_identifier(node, source) {
|
||||
Some(name) => {
|
||||
make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None)
|
||||
},
|
||||
None => group_candidate(node, ChunkKind::Let, source),
|
||||
}
|
||||
} else {
|
||||
group_candidate(node, ChunkKind::Let, source)
|
||||
// ── Variables (conditional auto-id vs group) ──
|
||||
"let_declaration" => Some(match extract_identifier(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Declarations, source),
|
||||
}),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_class_custom<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// ── Fields (conditional auto-id vs group) ──
|
||||
"field_declaration" => Some(match extract_identifier(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Field, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Fields, source),
|
||||
}),
|
||||
|
||||
// ── Enum variants (conditional auto-id vs group) ──
|
||||
"enum_variant" => Some(match extract_identifier(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Variant, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Variants, source),
|
||||
}),
|
||||
|
||||
// ── Attributes (explicitly return None — absorbed by framework) ──
|
||||
"attribute_item" => None,
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_function_custom<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
let fn_recurse = || recurse_body(node, ChunkContext::FunctionBody);
|
||||
match node.kind() {
|
||||
// ── Control flow with recurse ──
|
||||
"if_expression" => Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::If,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
None,
|
||||
fn_recurse(),
|
||||
source,
|
||||
)),
|
||||
|
||||
// ── Blocks ──
|
||||
"unsafe_block" | "async_block" | "const_block" | "block_expression" => Some(make_candidate(
|
||||
node,
|
||||
ChunkKind::Block,
|
||||
None,
|
||||
NameStyle::Named,
|
||||
None,
|
||||
fn_recurse(),
|
||||
source,
|
||||
)),
|
||||
|
||||
// ── Variables (conditional line span) ──
|
||||
"let_declaration" => {
|
||||
let span = line_span(node.start_position().row + 1, node.end_position().row + 1);
|
||||
Some(if span > 1 {
|
||||
match extract_identifier(node, source) {
|
||||
Some(name) => make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None),
|
||||
None => group_candidate(node, ChunkKind::Let, source),
|
||||
}
|
||||
},
|
||||
} else {
|
||||
group_candidate(node, ChunkKind::Let, source)
|
||||
})
|
||||
},
|
||||
|
||||
// ── Expression statements ──
|
||||
"expression_statement" => group_candidate(node, ChunkKind::Statements, source),
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,25 +2,42 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{ClassifierTables, LangClassifier, StructuralOverrides},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct SqlClassifier;
|
||||
|
||||
impl LangClassifier for SqlClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_sql_root(node, source)
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
static TABLES: ClassifierTables = ClassifierTables {
|
||||
root: &[],
|
||||
class: &[],
|
||||
function: &[],
|
||||
structural_overrides: StructuralOverrides {
|
||||
extra_trivia: &["empty_statement", "dollar_quote", "keyword_from"],
|
||||
preserved_trivia: &[],
|
||||
extra_root_wrappers: &[],
|
||||
preserved_root_wrappers: &[],
|
||||
absorbable_attrs: &[],
|
||||
},
|
||||
};
|
||||
&TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_sql_class(node, source)
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_sql_function(node, source)
|
||||
}
|
||||
|
||||
fn is_trivia(&self, kind: &str) -> bool {
|
||||
matches!(kind, "empty_statement" | "dollar_quote" | "keyword_from")
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match context {
|
||||
ChunkContext::Root => classify_sql_root(node, source),
|
||||
ChunkContext::ClassBody => classify_sql_class(node, source),
|
||||
ChunkContext::FunctionBody => classify_sql_function(node, source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,25 +2,39 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{ClassifierTables, LangClassifier, StructuralOverrides},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct SvelteClassifier;
|
||||
|
||||
impl LangClassifier for SvelteClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_svelte_node(node, source, true)
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
static TABLES: ClassifierTables = ClassifierTables {
|
||||
root: &[],
|
||||
class: &[],
|
||||
function: &[],
|
||||
structural_overrides: StructuralOverrides {
|
||||
extra_trivia: &[],
|
||||
preserved_trivia: &[],
|
||||
extra_root_wrappers: &["document"],
|
||||
preserved_root_wrappers: &[],
|
||||
absorbable_attrs: &[],
|
||||
},
|
||||
};
|
||||
&TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_svelte_node(node, source, false)
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_svelte_node(node, source, false)
|
||||
}
|
||||
|
||||
fn is_root_wrapper(&self, kind: &str) -> bool {
|
||||
kind == "document"
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
let include_plain_elements = matches!(context, ChunkContext::Root);
|
||||
classify_svelte_node(node, source, include_plain_elements)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,14 @@
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{
|
||||
classify::LangClassifier,
|
||||
classify::{
|
||||
ClassifierTables, LangClassifier, NamingMode, RecurseMode, RuleStyle, StructuralOverrides,
|
||||
semantic_rule,
|
||||
},
|
||||
common::{
|
||||
ChunkContext, RawChunkCandidate, RecurseSpec, child_by_kind, extract_identifier,
|
||||
group_candidate, make_container_chunk, make_container_chunk_from, make_kind_chunk,
|
||||
positional_candidate, recurse_self, sanitize_identifier,
|
||||
make_container_chunk, make_container_chunk_from, make_kind_chunk, recurse_self,
|
||||
sanitize_identifier,
|
||||
},
|
||||
kind::ChunkKind,
|
||||
types::ChunkNode,
|
||||
@@ -21,99 +24,104 @@ use super::{
|
||||
pub struct TlaplusClassifier;
|
||||
|
||||
impl LangClassifier for TlaplusClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
"module" => Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Module,
|
||||
tla_identifier(node, source),
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::Root)),
|
||||
)),
|
||||
"variable_declaration" | "constant_declaration" | "recursive_declaration" => {
|
||||
Some(group_candidate(node, ChunkKind::Declarations, source))
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
static TABLES: ClassifierTables = ClassifierTables {
|
||||
root: &[
|
||||
semantic_rule(
|
||||
"variable_declaration",
|
||||
ChunkKind::Declarations,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"constant_declaration",
|
||||
ChunkKind::Declarations,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"recursive_declaration",
|
||||
ChunkKind::Declarations,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
],
|
||||
class: &[semantic_rule(
|
||||
"pcal_var_decls",
|
||||
ChunkKind::Declarations,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
)],
|
||||
function: &[
|
||||
semantic_rule(
|
||||
"pcal_if",
|
||||
ChunkKind::If,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"pcal_while",
|
||||
ChunkKind::Loop,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"pcal_either",
|
||||
ChunkKind::Either,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"pcal_with",
|
||||
ChunkKind::With,
|
||||
RuleStyle::Positional,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
semantic_rule(
|
||||
"pcal_assign",
|
||||
ChunkKind::Statements,
|
||||
RuleStyle::Group,
|
||||
NamingMode::None,
|
||||
RecurseMode::None,
|
||||
),
|
||||
],
|
||||
structural_overrides: StructuralOverrides {
|
||||
extra_trivia: &[
|
||||
"header_line",
|
||||
"double_line",
|
||||
"extends",
|
||||
"pcal_algorithm_start",
|
||||
],
|
||||
preserved_trivia: &["block_comment"],
|
||||
extra_root_wrappers: &[],
|
||||
preserved_root_wrappers: &["module"],
|
||||
absorbable_attrs: &[],
|
||||
},
|
||||
"operator_definition" => Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Operator,
|
||||
tla_identifier(node, source),
|
||||
source,
|
||||
None,
|
||||
)),
|
||||
"module_definition" => Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Module,
|
||||
tla_identifier(node, source),
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::Root)),
|
||||
)),
|
||||
"pcal_algorithm" => Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Algo,
|
||||
tla_identifier(node, source),
|
||||
source,
|
||||
recurse_child(node, "pcal_algorithm_body", ChunkContext::ClassBody),
|
||||
)),
|
||||
"block_comment" => child_by_kind(node, &["pcal_algorithm"]).map(|algorithm| {
|
||||
make_container_chunk_from(
|
||||
node,
|
||||
algorithm,
|
||||
ChunkKind::Algo,
|
||||
tla_identifier(algorithm, source),
|
||||
source,
|
||||
recurse_child(algorithm, "pcal_algorithm_body", ChunkContext::ClassBody),
|
||||
)
|
||||
}),
|
||||
};
|
||||
&TABLES
|
||||
}
|
||||
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match context {
|
||||
ChunkContext::Root => classify_root_custom(node, source),
|
||||
ChunkContext::ClassBody => classify_class_custom(node, source),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
"pcal_procedure" => Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Proc,
|
||||
tla_identifier(node, source),
|
||||
source,
|
||||
recurse_child(node, "pcal_algorithm_body", ChunkContext::ClassBody),
|
||||
)),
|
||||
"pcal_process" => Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Process,
|
||||
tla_identifier(node, source),
|
||||
source,
|
||||
recurse_child(node, "pcal_algorithm_body", ChunkContext::ClassBody),
|
||||
)),
|
||||
"pcal_var_decls" => Some(group_candidate(node, ChunkKind::Declarations, source)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// PlusCal control flow
|
||||
"pcal_if" => Some(positional_candidate(node, ChunkKind::If, source)),
|
||||
"pcal_while" => Some(positional_candidate(node, ChunkKind::Loop, source)),
|
||||
"pcal_either" => Some(positional_candidate(node, ChunkKind::Either, source)),
|
||||
"pcal_with" => Some(positional_candidate(node, ChunkKind::With, source)),
|
||||
// PlusCal assignments
|
||||
"pcal_assign" => Some(group_candidate(node, ChunkKind::Statements, source)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn preserve_trivia(&self, kind: &str) -> bool {
|
||||
kind == "block_comment"
|
||||
}
|
||||
|
||||
fn is_trivia(&self, kind: &str) -> bool {
|
||||
matches!(kind, "header_line" | "double_line" | "extends" | "pcal_algorithm_start")
|
||||
}
|
||||
|
||||
fn preserve_root_wrapper(&self, kind: &str) -> bool {
|
||||
kind == "module"
|
||||
}
|
||||
|
||||
fn preserve_children(
|
||||
&self,
|
||||
parent: &RawChunkCandidate<'_>,
|
||||
@@ -184,6 +192,70 @@ impl LangClassifier for TlaplusClassifier {
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_root_custom<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
"module" => Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Module,
|
||||
tla_identifier(node, source),
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::Root)),
|
||||
)),
|
||||
"operator_definition" => Some(make_kind_chunk(
|
||||
node,
|
||||
ChunkKind::Operator,
|
||||
tla_identifier(node, source),
|
||||
source,
|
||||
None,
|
||||
)),
|
||||
"module_definition" => Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Module,
|
||||
tla_identifier(node, source),
|
||||
source,
|
||||
Some(recurse_self(node, ChunkContext::Root)),
|
||||
)),
|
||||
"pcal_algorithm" => Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Algo,
|
||||
tla_identifier(node, source),
|
||||
source,
|
||||
recurse_child(node, "pcal_algorithm_body", ChunkContext::ClassBody),
|
||||
)),
|
||||
"block_comment" => child_by_kind(node, &["pcal_algorithm"]).map(|algorithm| {
|
||||
make_container_chunk_from(
|
||||
node,
|
||||
algorithm,
|
||||
ChunkKind::Algo,
|
||||
tla_identifier(algorithm, source),
|
||||
source,
|
||||
recurse_child(algorithm, "pcal_algorithm_body", ChunkContext::ClassBody),
|
||||
)
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_class_custom<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
"pcal_procedure" => Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Proc,
|
||||
tla_identifier(node, source),
|
||||
source,
|
||||
recurse_child(node, "pcal_algorithm_body", ChunkContext::ClassBody),
|
||||
)),
|
||||
"pcal_process" => Some(make_container_chunk(
|
||||
node,
|
||||
ChunkKind::Process,
|
||||
tla_identifier(node, source),
|
||||
source,
|
||||
recurse_child(node, "pcal_algorithm_body", ChunkContext::ClassBody),
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn tla_identifier(node: Node<'_>, source: &str) -> Option<String> {
|
||||
extract_identifier(node, source).or_else(|| {
|
||||
child_by_kind(node, &["identifier"])
|
||||
|
||||
@@ -2,25 +2,41 @@
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
|
||||
use super::{
|
||||
classify::{ClassifierTables, LangClassifier, StructuralOverrides},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
|
||||
pub struct VueClassifier;
|
||||
|
||||
impl LangClassifier for VueClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_root_node(node, source)
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
static TABLES: ClassifierTables = ClassifierTables {
|
||||
root: &[],
|
||||
class: &[],
|
||||
function: &[],
|
||||
structural_overrides: StructuralOverrides {
|
||||
extra_trivia: &[],
|
||||
preserved_trivia: &[],
|
||||
extra_root_wrappers: &["document"],
|
||||
preserved_root_wrappers: &[],
|
||||
absorbable_attrs: &[],
|
||||
},
|
||||
};
|
||||
&TABLES
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_nested_node(node, source)
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
classify_nested_node(node, source)
|
||||
}
|
||||
|
||||
fn is_root_wrapper(&self, kind: &str) -> bool {
|
||||
kind == "document"
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
context: ChunkContext,
|
||||
node: Node<'t>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
match context {
|
||||
ChunkContext::Root => classify_root_node(node, source),
|
||||
ChunkContext::ClassBody | ChunkContext::FunctionBody => classify_nested_node(node, source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use super::schema;
|
||||
|
||||
type AtomSet = HashSet<&'static str>;
|
||||
|
||||
static ATOM_NODES: std::sync::LazyLock<HashMap<&'static str, AtomSet>> =
|
||||
std::sync::LazyLock::new(|| {
|
||||
HashMap::from([
|
||||
("astro", HashSet::from(["frontmatter"])),
|
||||
("bash", HashSet::from(["string", "raw_string", "heredoc_body", "simple_expansion"])),
|
||||
("c", HashSet::from(["string_literal", "char_literal"])),
|
||||
("clojure", HashSet::from(["kwd_lit", "regex_lit"])),
|
||||
("cmake", HashSet::from(["argument"])),
|
||||
("cpp", HashSet::from(["string_literal", "char_literal"])),
|
||||
(
|
||||
"csharp",
|
||||
HashSet::from([
|
||||
"string_literal",
|
||||
"verbatim_string_literal",
|
||||
"character_literal",
|
||||
"modifier",
|
||||
]),
|
||||
),
|
||||
("css", HashSet::from(["integer_value", "float_value", "color_value", "string_value"])),
|
||||
("elixir", HashSet::from(["string_constant_expr"])),
|
||||
("go", HashSet::from(["interpreted_string_literal", "raw_string_literal"])),
|
||||
(
|
||||
"haskell",
|
||||
HashSet::from([
|
||||
"qualified_variable",
|
||||
"qualified_module",
|
||||
"qualified_constructor",
|
||||
"strict_type",
|
||||
]),
|
||||
),
|
||||
("hcl", HashSet::from(["string_lit", "heredoc_template"])),
|
||||
(
|
||||
"html",
|
||||
HashSet::from(["doctype", "quoted_attribute_value", "raw_text", "tag_name", "text"]),
|
||||
),
|
||||
(
|
||||
"java",
|
||||
HashSet::from([
|
||||
"string_literal",
|
||||
"boolean_type",
|
||||
"integral_type",
|
||||
"floating_point_type",
|
||||
"void_type",
|
||||
]),
|
||||
),
|
||||
("json", HashSet::from(["string"])),
|
||||
(
|
||||
"julia",
|
||||
HashSet::from([
|
||||
"string_literal",
|
||||
"prefixed_string_literal",
|
||||
"command_literal",
|
||||
"character_literal",
|
||||
]),
|
||||
),
|
||||
(
|
||||
"kotlin",
|
||||
HashSet::from([
|
||||
"nullable_type",
|
||||
"string_literal",
|
||||
"line_string_literal",
|
||||
"character_literal",
|
||||
]),
|
||||
),
|
||||
("lua", HashSet::from(["string"])),
|
||||
("make", HashSet::from(["shell_text", "text"])),
|
||||
("nix", HashSet::from(["string_expression", "indented_string_expression"])),
|
||||
("objc", HashSet::from(["string_literal"])),
|
||||
(
|
||||
"perl",
|
||||
HashSet::from([
|
||||
"string_single_quoted",
|
||||
"string_double_quoted",
|
||||
"comments",
|
||||
"command_qx_quoted",
|
||||
"pattern_matcher_m",
|
||||
"regex_pattern_qr",
|
||||
"transliteration_tr_or_y",
|
||||
"substitution_pattern_s",
|
||||
"scalar_variable",
|
||||
"array_variable",
|
||||
"hash_variable",
|
||||
"hash_access_variable",
|
||||
]),
|
||||
),
|
||||
("php", HashSet::from(["string", "encapsed_string"])),
|
||||
("protobuf", HashSet::from(["string"])),
|
||||
("python", HashSet::from(["string"])),
|
||||
("r", HashSet::from(["string", "special"])),
|
||||
("ruby", HashSet::from(["string", "heredoc_body", "regex"])),
|
||||
("rust", HashSet::from(["char_literal", "string_literal", "raw_string_literal"])),
|
||||
("scala", HashSet::from(["string", "template_string", "interpolated_string_expression"])),
|
||||
("solidity", HashSet::from(["string", "hex_string_literal", "unicode_string_literal"])),
|
||||
("sql", HashSet::from(["string", "identifier"])),
|
||||
("swift", HashSet::from(["line_string_literal"])),
|
||||
("toml", HashSet::from(["string", "quoted_key"])),
|
||||
("tsx", HashSet::from(["string", "template_string"])),
|
||||
("typescript", HashSet::from(["string", "template_string", "regex", "predefined_type"])),
|
||||
("xml", HashSet::from(["AttValue", "XMLDecl"])),
|
||||
(
|
||||
"yaml",
|
||||
HashSet::from([
|
||||
"string_scalar",
|
||||
"double_quote_scalar",
|
||||
"single_quote_scalar",
|
||||
"block_scalar",
|
||||
]),
|
||||
),
|
||||
("verilog", HashSet::from(["integral_number"])),
|
||||
("zig", HashSet::from(["string"])),
|
||||
])
|
||||
});
|
||||
|
||||
pub fn is_atom_node(language: &str, kind: &str) -> bool {
|
||||
ATOM_NODES
|
||||
.get(language)
|
||||
.is_some_and(|atom_nodes| atom_nodes.contains(kind))
|
||||
}
|
||||
|
||||
pub fn is_atom_node_current(kind: &str) -> bool {
|
||||
schema::current_language().is_some_and(|language| is_atom_node(language, kind))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::is_atom_node;
|
||||
|
||||
#[test]
|
||||
fn nix_binding_set_is_not_an_atom() {
|
||||
assert!(!is_atom_node("nix", "binding_set"));
|
||||
assert!(is_atom_node("nix", "string_expression"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typescript_predefined_types_stay_atomic() {
|
||||
assert!(is_atom_node("typescript", "predefined_type"));
|
||||
assert!(!is_atom_node("typescript", "class_declaration"));
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,127 @@
|
||||
//! Per-language chunk classification trait.
|
||||
//!
|
||||
//! Each language module implements [`LangClassifier`] to provide
|
||||
//! language-specific node classification. The trait methods return `Option`
|
||||
//! — returning `None` falls through to the shared default classification in
|
||||
//! [`super::defaults`].
|
||||
//! Languages now provide semantic tables plus a narrow override hook for
|
||||
//! genuinely custom behavior.
|
||||
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::common::RawChunkCandidate;
|
||||
use super::{
|
||||
common::{
|
||||
ChunkContext, NameStyle, RawChunkCandidate, extract_identifier, make_candidate, recurse_self,
|
||||
resolve_recurse, resolve_value_container, sanitize_node_kind, signature_for_node,
|
||||
},
|
||||
kind::ChunkKind,
|
||||
};
|
||||
use crate::chunk::types::ChunkNode;
|
||||
|
||||
/// Language-specific chunk classification.
|
||||
///
|
||||
/// All methods have default no-op implementations so languages only need to
|
||||
/// override the ones they specialize.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum RuleStyle {
|
||||
Named,
|
||||
Group,
|
||||
Positional,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum NamingMode {
|
||||
AutoIdentifier,
|
||||
None,
|
||||
SanitizedKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum RecurseMode {
|
||||
None,
|
||||
Auto(ChunkContext),
|
||||
SelfNode(ChunkContext),
|
||||
ValueContainer,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct SemanticRule {
|
||||
pub ts_kind: &'static str,
|
||||
pub chunk_kind: ChunkKind,
|
||||
pub style: RuleStyle,
|
||||
pub naming: NamingMode,
|
||||
pub recurse: RecurseMode,
|
||||
}
|
||||
|
||||
pub const fn semantic_rule(
|
||||
ts_kind: &'static str,
|
||||
chunk_kind: ChunkKind,
|
||||
style: RuleStyle,
|
||||
naming: NamingMode,
|
||||
recurse: RecurseMode,
|
||||
) -> SemanticRule {
|
||||
SemanticRule { ts_kind, chunk_kind, style, naming, recurse }
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct StructuralOverrides {
|
||||
pub extra_trivia: &'static [&'static str],
|
||||
pub preserved_trivia: &'static [&'static str],
|
||||
pub extra_root_wrappers: &'static [&'static str],
|
||||
pub preserved_root_wrappers: &'static [&'static str],
|
||||
pub absorbable_attrs: &'static [&'static str],
|
||||
}
|
||||
|
||||
impl StructuralOverrides {
|
||||
pub const EMPTY: Self = Self {
|
||||
extra_trivia: &[],
|
||||
preserved_trivia: &[],
|
||||
extra_root_wrappers: &[],
|
||||
preserved_root_wrappers: &[],
|
||||
absorbable_attrs: &[],
|
||||
};
|
||||
|
||||
pub fn is_extra_trivia(&self, kind: &str) -> bool {
|
||||
self.extra_trivia.contains(&kind)
|
||||
}
|
||||
|
||||
pub fn preserves_trivia(&self, kind: &str) -> bool {
|
||||
self.preserved_trivia.contains(&kind)
|
||||
}
|
||||
|
||||
pub fn is_extra_root_wrapper(&self, kind: &str) -> bool {
|
||||
self.extra_root_wrappers.contains(&kind)
|
||||
}
|
||||
|
||||
pub fn preserves_root_wrapper(&self, kind: &str) -> bool {
|
||||
self.preserved_root_wrappers.contains(&kind)
|
||||
}
|
||||
|
||||
pub fn is_absorbable_attr(&self, kind: &str) -> bool {
|
||||
self.absorbable_attrs.contains(&kind)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ClassifierTables {
|
||||
pub root: &'static [SemanticRule],
|
||||
pub class: &'static [SemanticRule],
|
||||
pub function: &'static [SemanticRule],
|
||||
pub structural_overrides: StructuralOverrides,
|
||||
}
|
||||
|
||||
pub const EMPTY_CLASSIFIER_TABLES: ClassifierTables = ClassifierTables {
|
||||
root: &[],
|
||||
class: &[],
|
||||
function: &[],
|
||||
structural_overrides: StructuralOverrides::EMPTY,
|
||||
};
|
||||
|
||||
pub trait LangClassifier {
|
||||
/// Classify a root-level node. Return `None` to use shared defaults.
|
||||
fn tables(&self) -> &'static ClassifierTables {
|
||||
&EMPTY_CLASSIFIER_TABLES
|
||||
}
|
||||
|
||||
fn classify_root<'t>(&self, _node: Node<'t>, _source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Classify a node inside a class/struct/interface body.
|
||||
fn classify_class<'t>(&self, _node: Node<'t>, _source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Classify a node inside a function body.
|
||||
fn classify_function<'t>(
|
||||
&self,
|
||||
_node: Node<'t>,
|
||||
@@ -34,8 +130,35 @@ pub trait LangClassifier {
|
||||
None
|
||||
}
|
||||
|
||||
/// Allow a language to keep container children expanded even when the shared
|
||||
/// collapse heuristic would flatten them into a leaf preview.
|
||||
fn is_root_wrapper(&self, _kind: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn preserve_root_wrapper(&self, _kind: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn preserve_trivia(&self, _kind: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_trivia(&self, _kind: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_absorbable_attr(&self, _kind: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn classify_override<'t>(
|
||||
&self,
|
||||
_context: ChunkContext,
|
||||
_node: Node<'t>,
|
||||
_source: &str,
|
||||
) -> Option<RawChunkCandidate<'t>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn preserve_children(
|
||||
&self,
|
||||
_parent: &RawChunkCandidate<'_>,
|
||||
@@ -44,8 +167,6 @@ pub trait LangClassifier {
|
||||
false
|
||||
}
|
||||
|
||||
/// Post-process the chunk tree after initial construction.
|
||||
/// Used for structural transformations
|
||||
fn post_process(
|
||||
&self,
|
||||
_chunks: &mut Vec<ChunkNode>,
|
||||
@@ -53,36 +174,88 @@ pub trait LangClassifier {
|
||||
_source: &str,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Additional node kinds treated as root wrappers to flatten.
|
||||
fn is_root_wrapper(&self, _kind: &str) -> bool {
|
||||
false
|
||||
pub fn structural_overrides(classifier: &dyn LangClassifier) -> StructuralOverrides {
|
||||
classifier.tables().structural_overrides
|
||||
}
|
||||
|
||||
pub fn classify_with_tables<'tree>(
|
||||
classifier: &dyn LangClassifier,
|
||||
context: ChunkContext,
|
||||
node: Node<'tree>,
|
||||
source: &str,
|
||||
) -> Option<RawChunkCandidate<'tree>> {
|
||||
if let Some(candidate) = classifier.classify_override(context, node, source) {
|
||||
return Some(candidate);
|
||||
}
|
||||
|
||||
/// Shared root-wrapper kinds that this language wants to preserve as real
|
||||
/// chunks.
|
||||
fn preserve_root_wrapper(&self, _kind: &str) -> bool {
|
||||
false
|
||||
}
|
||||
find_rule(classifier.tables(), context, node.kind())
|
||||
.map(|rule| build_candidate_from_rule(node, source, *rule))
|
||||
.or_else(|| match context {
|
||||
ChunkContext::Root => classifier.classify_root(node, source),
|
||||
ChunkContext::ClassBody => classifier.classify_class(node, source),
|
||||
ChunkContext::FunctionBody => classifier.classify_function(node, source),
|
||||
})
|
||||
}
|
||||
|
||||
/// Allow a language to opt specific trivia nodes back into structural
|
||||
/// classification. Used when a grammar wraps real structure in comments.
|
||||
fn preserve_trivia(&self, _kind: &str) -> bool {
|
||||
false
|
||||
}
|
||||
pub fn build_candidate_from_rule<'tree>(
|
||||
node: Node<'tree>,
|
||||
source: &str,
|
||||
rule: SemanticRule,
|
||||
) -> RawChunkCandidate<'tree> {
|
||||
let identifier = match rule.naming {
|
||||
NamingMode::AutoIdentifier => extract_identifier(node, source),
|
||||
NamingMode::None => None,
|
||||
NamingMode::SanitizedKind => Some(sanitize_node_kind(node.kind()).to_string()),
|
||||
};
|
||||
|
||||
/// Additional node kinds treated as trivia (absorbed into adjacent chunks).
|
||||
fn is_trivia(&self, _kind: &str) -> bool {
|
||||
false
|
||||
}
|
||||
let recurse = match rule.recurse {
|
||||
RecurseMode::None => None,
|
||||
RecurseMode::Auto(context) => resolve_recurse(node, context),
|
||||
RecurseMode::SelfNode(context) => Some(recurse_self(node, context)),
|
||||
RecurseMode::ValueContainer => resolve_value_container(node),
|
||||
};
|
||||
|
||||
/// Additional node kinds treated as absorbable attributes (like Rust
|
||||
/// `#[derive(...)]`).
|
||||
fn is_absorbable_attr(&self, _kind: &str) -> bool {
|
||||
false
|
||||
match rule.style {
|
||||
RuleStyle::Named => make_candidate(
|
||||
node,
|
||||
rule.chunk_kind,
|
||||
identifier,
|
||||
NameStyle::Named,
|
||||
signature_for_node(node, source),
|
||||
recurse,
|
||||
source,
|
||||
),
|
||||
RuleStyle::Group => {
|
||||
make_candidate(node, rule.chunk_kind, identifier, NameStyle::Group, None, recurse, source)
|
||||
},
|
||||
RuleStyle::Positional => make_candidate(
|
||||
node,
|
||||
rule.chunk_kind,
|
||||
None::<String>,
|
||||
NameStyle::Named,
|
||||
None,
|
||||
recurse,
|
||||
source,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn find_rule(
|
||||
tables: &ClassifierTables,
|
||||
context: ChunkContext,
|
||||
kind: &str,
|
||||
) -> Option<&'static SemanticRule> {
|
||||
let rules = match context {
|
||||
ChunkContext::Root => tables.root,
|
||||
ChunkContext::ClassBody => tables.class,
|
||||
ChunkContext::FunctionBody => tables.function,
|
||||
};
|
||||
|
||||
rules.iter().find(|rule| rule.ts_kind == kind)
|
||||
}
|
||||
|
||||
/// Resolve a [`LangClassifier`] for the given language.
|
||||
pub fn classifier_for(lang: &str) -> &'static dyn LangClassifier {
|
||||
match lang {
|
||||
|
||||
@@ -8,6 +8,7 @@ use tree_sitter::Node;
|
||||
|
||||
use super::{
|
||||
kind::{ChunkKind, SummaryStyle},
|
||||
shape,
|
||||
types::ChunkNode,
|
||||
};
|
||||
use crate::env_uint;
|
||||
@@ -129,7 +130,7 @@ pub fn make_candidate<'tree>(
|
||||
error: kind == ChunkKind::Error,
|
||||
groupable: kind.traits().groupable,
|
||||
has_leading_comment: false,
|
||||
force_recurse: kind.traits().container,
|
||||
force_recurse: kind.traits().container || (recurse.is_some() && node.has_error()),
|
||||
recurse,
|
||||
}
|
||||
}
|
||||
@@ -254,15 +255,31 @@ pub fn prefixed_name(prefix: &str, node: Node<'_>, source: &str) -> String {
|
||||
pub fn infer_named_candidate<'tree>(node: Node<'tree>, source: &str) -> RawChunkCandidate<'tree> {
|
||||
let kind_name = sanitize_node_kind(node.kind());
|
||||
let kind = ChunkKind::from_sanitized_kind(kind_name);
|
||||
make_kind_chunk(node, kind, extract_identifier(node, source), source, None)
|
||||
auto_classify(node, kind, source)
|
||||
}
|
||||
|
||||
pub fn auto_classify<'tree>(
|
||||
node: Node<'tree>,
|
||||
kind: ChunkKind,
|
||||
source: &str,
|
||||
) -> RawChunkCandidate<'tree> {
|
||||
make_kind_chunk(
|
||||
node,
|
||||
kind,
|
||||
extract_identifier(node, source),
|
||||
source,
|
||||
auto_recurse_for_kind(node, kind),
|
||||
)
|
||||
}
|
||||
|
||||
// ── Tree navigation helpers ──────────────────────────────────────────────
|
||||
|
||||
pub fn named_children(node: Node<'_>) -> Vec<Node<'_>> {
|
||||
let mut children = Vec::new();
|
||||
for index in 0..node.named_child_count() {
|
||||
if let Some(child) = node.named_child(index) {
|
||||
for index in 0..node.child_count() {
|
||||
if let Some(child) = node.child(index)
|
||||
&& (child.is_named() || child.is_error() || child.kind() == "ERROR")
|
||||
{
|
||||
children.push(child);
|
||||
}
|
||||
}
|
||||
@@ -288,6 +305,30 @@ pub fn child_by_field_or_kind<'tree>(
|
||||
child_by_kind(node, kinds)
|
||||
}
|
||||
|
||||
pub fn resolve_recurse(node: Node<'_>, context: ChunkContext) -> Option<RecurseSpec<'_>> {
|
||||
shape::recurse_target(node).map(|child| RecurseSpec { node: child, context })
|
||||
}
|
||||
|
||||
pub fn resolve_value_container(node: Node<'_>) -> Option<RecurseSpec<'_>> {
|
||||
shape::value_container_target(node)
|
||||
.map(|child| RecurseSpec { node: child, context: ChunkContext::ClassBody })
|
||||
}
|
||||
|
||||
fn auto_recurse_for_kind(node: Node<'_>, kind: ChunkKind) -> Option<RecurseSpec<'_>> {
|
||||
let context = match kind {
|
||||
ChunkKind::Constructor
|
||||
| ChunkKind::Function
|
||||
| ChunkKind::Macro
|
||||
| ChunkKind::Method
|
||||
| ChunkKind::Proc
|
||||
| ChunkKind::Recipe => ChunkContext::FunctionBody,
|
||||
_ if kind.traits().container => ChunkContext::ClassBody,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
resolve_recurse(node, context)
|
||||
}
|
||||
|
||||
pub fn compute_body_inner_boundaries(
|
||||
source: &str,
|
||||
body_start: usize,
|
||||
@@ -366,104 +407,23 @@ pub const fn recurse_self(node: Node<'_>, context: ChunkContext) -> RecurseSpec<
|
||||
}
|
||||
|
||||
pub fn recurse_body(node: Node<'_>, context: ChunkContext) -> Option<RecurseSpec<'_>> {
|
||||
recurse_into(node, context, &["body"], &[
|
||||
"statement_block",
|
||||
"compound_statement",
|
||||
"function_body",
|
||||
"constructor_body",
|
||||
"do_block",
|
||||
"do_group",
|
||||
"block",
|
||||
"body_statement",
|
||||
"statements",
|
||||
"recipe",
|
||||
"yul_block",
|
||||
])
|
||||
resolve_recurse(node, context)
|
||||
}
|
||||
|
||||
pub fn recurse_class(node: Node<'_>) -> Option<RecurseSpec<'_>> {
|
||||
recurse_into(node, ChunkContext::ClassBody, &["body"], &[
|
||||
"class_body",
|
||||
"interface_body",
|
||||
"enum_body",
|
||||
"protocol_body",
|
||||
"declaration_list",
|
||||
"implementation_definition",
|
||||
"contract_body",
|
||||
"struct_body",
|
||||
"keyframe_block_list",
|
||||
"block",
|
||||
"body_statement",
|
||||
"body",
|
||||
"enum_class_body",
|
||||
])
|
||||
resolve_recurse(node, ChunkContext::ClassBody)
|
||||
}
|
||||
|
||||
pub fn recurse_interface(node: Node<'_>) -> Option<RecurseSpec<'_>> {
|
||||
recurse_into(node, ChunkContext::ClassBody, &["body"], &[
|
||||
"object_type",
|
||||
"interface_body",
|
||||
"protocol_body",
|
||||
"contract_body",
|
||||
"struct_body",
|
||||
"block",
|
||||
"body",
|
||||
])
|
||||
resolve_recurse(node, ChunkContext::ClassBody)
|
||||
}
|
||||
|
||||
pub fn recurse_enum(node: Node<'_>) -> Option<RecurseSpec<'_>> {
|
||||
recurse_into(node, ChunkContext::ClassBody, &["body"], &[
|
||||
"enum_body",
|
||||
"block",
|
||||
"body",
|
||||
"struct_body",
|
||||
])
|
||||
}
|
||||
|
||||
/// Node kinds that represent a container value in a key-value pair.
|
||||
fn is_value_container_kind(kind: &str) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
"object"
|
||||
| "array"
|
||||
| "inline_table"
|
||||
| "table"
|
||||
| "table_array_element"
|
||||
| "block_mapping"
|
||||
| "block_sequence"
|
||||
| "flow_mapping"
|
||||
| "flow_sequence"
|
||||
| "block"
|
||||
| "attrset_expression"
|
||||
| "let_expression"
|
||||
| "function_expression"
|
||||
| "body"
|
||||
| "binding_set"
|
||||
)
|
||||
resolve_recurse(node, ChunkContext::ClassBody)
|
||||
}
|
||||
|
||||
pub fn recurse_value_container(node: Node<'_>) -> Option<RecurseSpec<'_>> {
|
||||
let children = named_children(node);
|
||||
// Direct container child (JSON objects/arrays, TOML tables, Nix attrsets).
|
||||
if let Some(container) = children
|
||||
.iter()
|
||||
.find(|child| is_value_container_kind(child.kind()))
|
||||
{
|
||||
return Some(RecurseSpec { node: *container, context: ChunkContext::ClassBody });
|
||||
}
|
||||
// YAML wraps values in `block_node` / `flow_node` before the actual
|
||||
// container. Unwrap one level so we find the `block_mapping`,
|
||||
// `block_sequence`, etc. inside.
|
||||
for child in &children {
|
||||
if matches!(child.kind(), "block_node" | "flow_node")
|
||||
&& let Some(inner) = named_children(*child)
|
||||
.into_iter()
|
||||
.find(|c| is_value_container_kind(c.kind()))
|
||||
{
|
||||
return Some(RecurseSpec { node: inner, context: ChunkContext::ClassBody });
|
||||
}
|
||||
}
|
||||
None
|
||||
resolve_value_container(node)
|
||||
}
|
||||
|
||||
// ── Identifier extraction ────────────────────────────────────────────────
|
||||
@@ -473,45 +433,11 @@ pub fn extract_identifier(node: Node<'_>, source: &str) -> Option<String> {
|
||||
return Some("constructor".to_string());
|
||||
}
|
||||
|
||||
let field_name = node.child_by_field_name("name").or_else(|| {
|
||||
child_by_kind(node, &[
|
||||
"identifier",
|
||||
"property_identifier",
|
||||
"private_property_identifier",
|
||||
"type_identifier",
|
||||
"field_identifier",
|
||||
"simple_identifier",
|
||||
"word",
|
||||
"symbol",
|
||||
"bare_key",
|
||||
"quoted_key",
|
||||
"dotted_key",
|
||||
])
|
||||
});
|
||||
if let Some(name_node) = field_name {
|
||||
if let Some(name_node) = shape::identifier_node(node) {
|
||||
return sanitize_identifier(node_text(source, name_node.start_byte(), name_node.end_byte()));
|
||||
}
|
||||
|
||||
named_children(node)
|
||||
.into_iter()
|
||||
.find(|child| {
|
||||
matches!(
|
||||
child.kind(),
|
||||
"identifier"
|
||||
| "property_identifier"
|
||||
| "private_property_identifier"
|
||||
| "type_identifier"
|
||||
| "field_identifier"
|
||||
| "simple_identifier"
|
||||
| "word" | "symbol"
|
||||
| "bare_key"
|
||||
| "quoted_key"
|
||||
| "dotted_key"
|
||||
)
|
||||
})
|
||||
.and_then(|child| {
|
||||
sanitize_identifier(node_text(source, child.start_byte(), child.end_byte()))
|
||||
})
|
||||
None
|
||||
}
|
||||
|
||||
/// Extract the name of a single-declarator binding like `const FOO = ...`.
|
||||
@@ -563,6 +489,7 @@ pub fn unquote_text(text: &str) -> String {
|
||||
|
||||
pub fn sanitize_node_kind(kind: &str) -> &str {
|
||||
let kind_stripped = kind
|
||||
.trim_suffix("_instruction")
|
||||
.trim_suffix("_statement")
|
||||
.trim_suffix("_declaration")
|
||||
.trim_suffix("_definition")
|
||||
@@ -615,48 +542,9 @@ pub fn collapse_whitespace(text: &str) -> String {
|
||||
|
||||
// ── Signature helpers ────────────────────────────────────────────────────
|
||||
|
||||
/// Kinds that represent "body" blocks — the signature is everything before
|
||||
/// them.
|
||||
pub const BODY_KINDS: &[&str] = &[
|
||||
"statement_block",
|
||||
"compound_statement",
|
||||
"function_body",
|
||||
"constructor_body",
|
||||
"do_block",
|
||||
"do_group",
|
||||
"block",
|
||||
"body_statement",
|
||||
"statements",
|
||||
"recipe",
|
||||
"yul_block",
|
||||
"class_body",
|
||||
"interface_body",
|
||||
"enum_body",
|
||||
"protocol_body",
|
||||
"declaration_list",
|
||||
"implementation_definition",
|
||||
"contract_body",
|
||||
"struct_body",
|
||||
"keyframe_block_list",
|
||||
"body",
|
||||
"enum_class_body",
|
||||
"object_type",
|
||||
"field_declaration_list",
|
||||
"method_spec_list",
|
||||
];
|
||||
|
||||
pub fn signature_for_node(node: Node<'_>, source: &str) -> Option<String> {
|
||||
let body_child = node
|
||||
.child_by_field_name("body")
|
||||
.filter(|c| c.start_byte() > node.start_byte())
|
||||
.or_else(|| {
|
||||
named_children(node)
|
||||
.into_iter()
|
||||
.find(|c| BODY_KINDS.contains(&c.kind()))
|
||||
});
|
||||
|
||||
let raw = if let Some(body) = body_child {
|
||||
node_text(source, node.start_byte(), body.start_byte())
|
||||
let raw = if let Some(end_byte) = shape::signature_end_byte(node) {
|
||||
node_text(source, node.start_byte(), end_byte)
|
||||
} else {
|
||||
node_text(source, node.start_byte(), node.end_byte())
|
||||
};
|
||||
@@ -797,30 +685,15 @@ fn rust_function_signature(header: &str) -> Option<String> {
|
||||
// ── Trivia and attribute detection ───────────────────────────────────────
|
||||
|
||||
pub fn is_trivia(kind: &str) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
"comment"
|
||||
| "decorator"
|
||||
| "line_comment"
|
||||
| "block_comment"
|
||||
| "start_tag"
|
||||
| "end_tag"
|
||||
| "comment_statement"
|
||||
| "element_node_start"
|
||||
| "element_node_end"
|
||||
| "element_node_void"
|
||||
| "block_statement_start"
|
||||
| "block_statement_end"
|
||||
| "xml_decl"
|
||||
| "else_directive"
|
||||
| "elsif_directive"
|
||||
| "attribute_item"
|
||||
| "inner_attribute_item"
|
||||
)
|
||||
shape::is_generic_trivia_name(kind)
|
||||
}
|
||||
|
||||
pub fn is_trivia_node(node: Node<'_>) -> bool {
|
||||
shape::is_generic_trivia(node)
|
||||
}
|
||||
|
||||
pub fn is_absorbable_attribute(kind: &str) -> bool {
|
||||
matches!(kind, "attribute_item" | "inner_attribute_item")
|
||||
shape::is_generic_absorbable_attr(kind)
|
||||
}
|
||||
|
||||
// ── Other helpers ────────────────────────────────────────────────────────
|
||||
@@ -858,33 +731,8 @@ pub fn detect_indent(source: &str, start_byte: usize) -> (u32, String) {
|
||||
(cols, ch)
|
||||
}
|
||||
|
||||
pub fn is_root_wrapper_kind(kind: &str) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
"body"
|
||||
| "block_node"
|
||||
| "flow_node"
|
||||
| "object"
|
||||
| "array"
|
||||
| "binding_set"
|
||||
| "block_mapping"
|
||||
| "flow_mapping"
|
||||
| "block_sequence"
|
||||
| "flow_sequence"
|
||||
| "program"
|
||||
| "source"
|
||||
| "source_code"
|
||||
| "source_file"
|
||||
| "stream"
|
||||
| "template"
|
||||
| "document"
|
||||
| "config_file"
|
||||
| "module"
|
||||
| "makefile"
|
||||
| "stylesheet"
|
||||
| "translation_unit"
|
||||
| "compilation_unit"
|
||||
)
|
||||
pub fn is_root_wrapper_node(node: Node<'_>) -> bool {
|
||||
shape::is_root_wrapper_node(node)
|
||||
}
|
||||
|
||||
pub const fn line_span(start_line: usize, end_line: usize) -> usize {
|
||||
|
||||
@@ -21,7 +21,13 @@ pub fn classify_function_default<'tree>(
|
||||
source: &str,
|
||||
) -> RawChunkCandidate<'tree> {
|
||||
let kind_name = sanitize_node_kind(node.kind());
|
||||
group_candidate(node, ChunkKind::from_sanitized_kind(kind_name), source)
|
||||
let kind = ChunkKind::from_sanitized_kind(kind_name);
|
||||
let candidate = auto_classify(node, kind, source);
|
||||
if candidate.recurse.is_some() || candidate.identifier.is_some() {
|
||||
candidate
|
||||
} else {
|
||||
group_candidate(node, kind, source)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn classify_var_decl<'tree>(node: Node<'tree>, source: &str) -> RawChunkCandidate<'tree> {
|
||||
|
||||
@@ -2,9 +2,9 @@ use std::{collections::HashMap, path::Path};
|
||||
|
||||
use crate::chunk::{
|
||||
indent::{
|
||||
denormalize_from_tabs, detect_file_indent_char, detect_file_indent_step,
|
||||
normalize_leading_whitespace_char, normalize_to_tabs, reindent_inserted_block,
|
||||
strip_content_prefixes,
|
||||
dedent_python_style, denormalize_from_tabs, detect_file_indent_char, detect_file_indent_step,
|
||||
indent_non_empty_lines, normalize_leading_whitespace_char, normalize_to_tabs,
|
||||
reindent_inserted_block, strip_content_prefixes,
|
||||
},
|
||||
kind::ChunkKind,
|
||||
resolve::{
|
||||
@@ -52,6 +52,9 @@ struct ResolvedEditTarget {
|
||||
region: Option<ChunkRegion>,
|
||||
}
|
||||
|
||||
const NORMALIZED_TAB_REPLACEMENT: &str = " ";
|
||||
const PRESERVED_TAB_REPLACEMENT: &str = "\t";
|
||||
|
||||
pub fn apply_edits(state: &ChunkState, params: &EditParams) -> Result<EditResult, String> {
|
||||
let original_text = normalize_chunk_source(state.inner().source());
|
||||
let initial_notebook_ctx = state.inner().notebook.clone();
|
||||
@@ -67,6 +70,7 @@ pub fn apply_edits(state: &ChunkState, params: &EditParams) -> Result<EditResult
|
||||
let initial_parse_errors = state.tree.parse_errors;
|
||||
let initial_chunk_paths: std::collections::HashSet<String> =
|
||||
state.tree.chunks.iter().map(|c| c.path.clone()).collect();
|
||||
let normalize_indent = params.normalize_indent.unwrap_or(true);
|
||||
let mut touched_paths = Vec::new();
|
||||
let mut warnings = Vec::new();
|
||||
let mut last_scheduled: Option<ScheduledEditOperation> = None;
|
||||
@@ -108,6 +112,7 @@ pub fn apply_edits(state: &ChunkState, params: &EditParams) -> Result<EditResult
|
||||
current_default_crc.as_deref(),
|
||||
file_indent_step,
|
||||
file_indent_char,
|
||||
normalize_indent,
|
||||
&mut touched_paths,
|
||||
&mut warnings,
|
||||
),
|
||||
@@ -129,6 +134,7 @@ pub fn apply_edits(state: &ChunkState, params: &EditParams) -> Result<EditResult
|
||||
current_default_crc.as_deref(),
|
||||
file_indent_step,
|
||||
file_indent_char,
|
||||
normalize_indent,
|
||||
&mut touched_paths,
|
||||
&mut warnings,
|
||||
)
|
||||
@@ -138,7 +144,8 @@ pub fn apply_edits(state: &ChunkState, params: &EditParams) -> Result<EditResult
|
||||
if let Err(err) = result {
|
||||
let display_path = display_path_for_file(¶ms.file_path, ¶ms.cwd);
|
||||
let sel = operation.sel.as_deref().or(current_default_selector);
|
||||
let context = render_error_context(&state, sel, &display_path, params.anchor_style);
|
||||
let context =
|
||||
render_error_context(&state, sel, &display_path, params.anchor_style, normalize_indent);
|
||||
return Err(format!(
|
||||
"Edit operation {}/{} failed ({}): {}\nNo changes were saved. Fix the failing \
|
||||
operation and retry the entire batch.{context}",
|
||||
@@ -202,7 +209,8 @@ pub fn apply_edits(state: &ChunkState, params: &EditParams) -> Result<EditResult
|
||||
.as_ref()
|
||||
.and_then(|s| s.operation.sel.as_deref())
|
||||
.or(initial_default_selector.as_deref());
|
||||
let context = render_error_context(&state, sel, &display_path, params.anchor_style);
|
||||
let context =
|
||||
render_error_context(&state, sel, &display_path, params.anchor_style, normalize_indent);
|
||||
return Err(format!(
|
||||
"Edit rejected: introduced {} parse error(s). The file was valid before the edit but is \
|
||||
not after. Fix the content and retry.{details}{context}",
|
||||
@@ -271,9 +279,10 @@ pub fn apply_edits(state: &ChunkState, params: &EditParams) -> Result<EditResult
|
||||
&diff_after,
|
||||
params.anchor_style,
|
||||
&touched_paths,
|
||||
normalize_indent,
|
||||
)
|
||||
} else {
|
||||
render_unchanged_response(&state, &display_path, params.anchor_style)
|
||||
render_unchanged_response(&state, &display_path, params.anchor_style, normalize_indent)
|
||||
};
|
||||
|
||||
Ok(EditResult {
|
||||
@@ -340,6 +349,7 @@ fn apply_replace(
|
||||
default_crc: Option<&str>,
|
||||
file_indent_step: usize,
|
||||
file_indent_char: char,
|
||||
normalize_indent: bool,
|
||||
touched_paths: &mut Vec<String>,
|
||||
warnings: &mut Vec<String>,
|
||||
) -> Result<(), String> {
|
||||
@@ -411,8 +421,13 @@ fn apply_replace(
|
||||
let target_indent =
|
||||
target_indent_for_region(state, &anchor, target.region, file_indent_char, file_indent_step);
|
||||
let content = operation.content.as_deref().unwrap_or_default();
|
||||
let mut replacement =
|
||||
normalize_inserted_content(content, &target_indent, Some(file_indent_step), file_indent_char);
|
||||
let mut replacement = normalize_inserted_content(
|
||||
content,
|
||||
&target_indent,
|
||||
Some(file_indent_step),
|
||||
file_indent_char,
|
||||
normalize_indent,
|
||||
);
|
||||
if target.region.is_none() {
|
||||
if !replacement.is_empty()
|
||||
&& !replacement.ends_with('\n')
|
||||
@@ -559,6 +574,7 @@ fn apply_insert(
|
||||
default_crc: Option<&str>,
|
||||
file_indent_step: usize,
|
||||
file_indent_char: char,
|
||||
normalize_indent: bool,
|
||||
touched_paths: &mut Vec<String>,
|
||||
warnings: &mut Vec<String>,
|
||||
) -> Result<(), String> {
|
||||
@@ -602,6 +618,7 @@ fn apply_insert(
|
||||
&insertion.indent,
|
||||
Some(file_indent_step),
|
||||
file_indent_char,
|
||||
normalize_indent,
|
||||
);
|
||||
replacement =
|
||||
normalize_insertion_boundary_content(state, insertion.offset, &replacement, spacing);
|
||||
@@ -895,9 +912,14 @@ fn normalize_inserted_content(
|
||||
target_indent: &str,
|
||||
file_indent_step: Option<usize>,
|
||||
file_indent_char: char,
|
||||
normalize_indent: bool,
|
||||
) -> String {
|
||||
let mut normalized = normalize_chunk_source(content);
|
||||
normalized = strip_content_prefixes(&normalized);
|
||||
if !normalize_indent {
|
||||
let dedented = dedent_python_style(&normalized);
|
||||
return indent_non_empty_lines(&dedented, target_indent);
|
||||
}
|
||||
normalized = normalized
|
||||
.split('\n')
|
||||
.map(|line| denormalize_from_tabs(line, file_indent_char, file_indent_step.unwrap_or(1)))
|
||||
@@ -1539,7 +1561,15 @@ struct DiffHunk {
|
||||
|
||||
/// Normalize the content part of a diff hunk line (after the +/-/space prefix)
|
||||
/// so that its indentation matches the chunk tree display format.
|
||||
fn normalize_hunk_line(line: &str, indent_char: char, indent_step: usize) -> String {
|
||||
fn render_hunk_line(
|
||||
line: &str,
|
||||
normalize_indent: bool,
|
||||
indent_char: char,
|
||||
indent_step: usize,
|
||||
) -> String {
|
||||
if !normalize_indent {
|
||||
return line.to_owned();
|
||||
}
|
||||
if line.is_empty() {
|
||||
return line.to_owned();
|
||||
}
|
||||
@@ -1599,6 +1629,7 @@ fn render_changed_hunks(
|
||||
after: &str,
|
||||
anchor_style: Option<ChunkAnchorStyle>,
|
||||
touched_paths: &[String],
|
||||
normalize_indent: bool,
|
||||
) -> String {
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -1610,11 +1641,16 @@ fn render_changed_hunks(
|
||||
// Walk from the deepest containing chunk upward until we find one that
|
||||
// has children (and therefore a closing tag in the tree output).
|
||||
let tree = state.tree();
|
||||
let tab_replacement = " ";
|
||||
let tab_replacement = if normalize_indent {
|
||||
NORMALIZED_TAB_REPLACEMENT
|
||||
} else {
|
||||
PRESERVED_TAB_REPLACEMENT
|
||||
};
|
||||
let file_indent_char = detect_file_indent_char(state.source(), tree);
|
||||
let file_indent_step = detect_file_indent_step(state.source(), tree) as usize;
|
||||
let lookup: HashMap<&str, &ChunkNode> =
|
||||
tree.chunks.iter().map(|c| (c.path.as_str(), c)).collect();
|
||||
let render_indent = normalize_indent.then_some((file_indent_char, file_indent_step));
|
||||
|
||||
let mut inline_hunks: HashMap<String, Vec<crate::chunk::render::InlineHunk>> = HashMap::new();
|
||||
let mut orphan_hunks: Vec<&DiffHunk> = Vec::new();
|
||||
@@ -1629,12 +1665,13 @@ fn render_changed_hunks(
|
||||
chunk_path,
|
||||
state.source(),
|
||||
tab_replacement,
|
||||
Some((file_indent_char, file_indent_step)),
|
||||
render_indent,
|
||||
);
|
||||
let mut lines = Vec::with_capacity(hunk.lines.len() + 1);
|
||||
lines.push(format!("{indent}{}", hunk.header));
|
||||
for line in &hunk.lines {
|
||||
let normalized = normalize_hunk_line(line, file_indent_char, file_indent_step);
|
||||
let normalized =
|
||||
render_hunk_line(line, normalize_indent, file_indent_char, file_indent_step);
|
||||
lines.push(format!("{indent}{normalized}"));
|
||||
}
|
||||
inline_hunks
|
||||
@@ -1658,7 +1695,7 @@ fn render_changed_hunks(
|
||||
anchor_style,
|
||||
show_leaf_preview,
|
||||
tab_replacement: Some(tab_replacement.to_owned()),
|
||||
normalize_indent: Some(true),
|
||||
normalize_indent: Some(normalize_indent),
|
||||
focused_paths,
|
||||
},
|
||||
inline_hunks,
|
||||
@@ -1674,12 +1711,9 @@ fn render_changed_hunks(
|
||||
.flat_map(|hunk| {
|
||||
let mut lines = Vec::with_capacity(hunk.lines.len() + 1);
|
||||
lines.push(hunk.header.clone());
|
||||
lines.extend(
|
||||
hunk
|
||||
.lines
|
||||
.iter()
|
||||
.map(|line| normalize_hunk_line(line, file_indent_char, file_indent_step)),
|
||||
);
|
||||
lines.extend(hunk.lines.iter().map(|line| {
|
||||
render_hunk_line(line, normalize_indent, file_indent_char, file_indent_step)
|
||||
}));
|
||||
lines
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
@@ -1760,6 +1794,7 @@ fn render_error_context(
|
||||
selector: Option<&str>,
|
||||
display_path: &str,
|
||||
anchor_style: Option<ChunkAnchorStyle>,
|
||||
normalize_indent: bool,
|
||||
) -> String {
|
||||
let Ok(ParsedSelector { selector: clean_path, .. }) =
|
||||
split_selector_crc_and_region(selector, None, None)
|
||||
@@ -1771,6 +1806,11 @@ fn render_error_context(
|
||||
return String::new();
|
||||
};
|
||||
let focused_paths = compute_focus(state.tree(), std::slice::from_ref(&chunk.path));
|
||||
let tab_replacement = if normalize_indent {
|
||||
NORMALIZED_TAB_REPLACEMENT
|
||||
} else {
|
||||
PRESERVED_TAB_REPLACEMENT
|
||||
};
|
||||
let rendered = crate::chunk::render::render_state(state, &RenderParams {
|
||||
chunk_path: Some(String::new()),
|
||||
title: display_path.to_owned(),
|
||||
@@ -1780,8 +1820,8 @@ fn render_error_context(
|
||||
omit_checksum: false,
|
||||
anchor_style,
|
||||
show_leaf_preview: true,
|
||||
tab_replacement: Some(" ".to_owned()),
|
||||
normalize_indent: Some(true),
|
||||
tab_replacement: Some(tab_replacement.to_owned()),
|
||||
normalize_indent: Some(normalize_indent),
|
||||
focused_paths,
|
||||
});
|
||||
format!("\n\nFresh content:\n{rendered}")
|
||||
@@ -1791,7 +1831,13 @@ fn render_unchanged_response(
|
||||
state: &ChunkStateInner,
|
||||
display_path: &str,
|
||||
anchor_style: Option<ChunkAnchorStyle>,
|
||||
normalize_indent: bool,
|
||||
) -> String {
|
||||
let tab_replacement = if normalize_indent {
|
||||
NORMALIZED_TAB_REPLACEMENT
|
||||
} else {
|
||||
PRESERVED_TAB_REPLACEMENT
|
||||
};
|
||||
crate::chunk::render::render_state(state, &RenderParams {
|
||||
chunk_path: Some(String::new()),
|
||||
title: display_path.to_owned(),
|
||||
@@ -1802,8 +1848,8 @@ fn render_unchanged_response(
|
||||
anchor_style,
|
||||
focused_paths: None,
|
||||
show_leaf_preview: true,
|
||||
tab_replacement: Some(" ".to_owned()),
|
||||
normalize_indent: Some(true),
|
||||
tab_replacement: Some(tab_replacement.to_owned()),
|
||||
normalize_indent: Some(normalize_indent),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1833,6 +1879,7 @@ mod tests {
|
||||
anchor_style: None,
|
||||
cwd: ".".to_owned(),
|
||||
file_path: file_path.to_owned(),
|
||||
normalize_indent: None,
|
||||
})
|
||||
.expect("edit should apply")
|
||||
}
|
||||
@@ -1857,6 +1904,7 @@ mod tests {
|
||||
anchor_style: None,
|
||||
cwd: ".".to_owned(),
|
||||
file_path: "test.rs".to_owned(),
|
||||
normalize_indent: None,
|
||||
})
|
||||
.expect("edit should apply");
|
||||
|
||||
@@ -1901,6 +1949,7 @@ mod tests {
|
||||
anchor_style: None,
|
||||
cwd: ".".to_owned(),
|
||||
file_path: "test.ts".to_owned(),
|
||||
normalize_indent: None,
|
||||
})
|
||||
.expect("edit should apply");
|
||||
|
||||
@@ -1973,6 +2022,7 @@ mod tests {
|
||||
anchor_style: None,
|
||||
cwd: ".".to_owned(),
|
||||
file_path: "test.ts".to_owned(),
|
||||
normalize_indent: None,
|
||||
})
|
||||
.expect("edit should resolve a unique fuzzy selector");
|
||||
|
||||
@@ -2014,6 +2064,7 @@ mod tests {
|
||||
anchor_style: None,
|
||||
cwd: ".".to_owned(),
|
||||
file_path: "box.ts".to_owned(),
|
||||
normalize_indent: None,
|
||||
})
|
||||
.expect("edit should resolve a prefixed bare selector");
|
||||
|
||||
@@ -2046,6 +2097,7 @@ mod tests {
|
||||
anchor_style: None,
|
||||
cwd: ".".to_owned(),
|
||||
file_path: "box.ts".to_owned(),
|
||||
normalize_indent: None,
|
||||
})
|
||||
.expect("edit should resolve file-prefixed checksum target");
|
||||
|
||||
@@ -2073,6 +2125,7 @@ mod tests {
|
||||
anchor_style: None,
|
||||
cwd: ".".to_owned(),
|
||||
file_path: "box.ts".to_owned(),
|
||||
normalize_indent: None,
|
||||
});
|
||||
|
||||
let result = result.expect("line-number selector should auto-resolve");
|
||||
@@ -2111,6 +2164,7 @@ mod tests {
|
||||
anchor_style: None,
|
||||
cwd: ".".to_owned(),
|
||||
file_path: "box.ts".to_owned(),
|
||||
normalize_indent: None,
|
||||
});
|
||||
|
||||
assert!(result.is_err(), "line outside any chunk should fail");
|
||||
@@ -2141,6 +2195,7 @@ mod tests {
|
||||
anchor_style: None,
|
||||
cwd: ".".to_owned(),
|
||||
file_path: "test.md".to_owned(),
|
||||
normalize_indent: None,
|
||||
})
|
||||
.expect("replace should succeed");
|
||||
|
||||
@@ -2176,6 +2231,7 @@ mod tests {
|
||||
anchor_style: None,
|
||||
cwd: ".".to_owned(),
|
||||
file_path: "test.rs".to_owned(),
|
||||
normalize_indent: None,
|
||||
})
|
||||
.expect("edit should apply");
|
||||
|
||||
@@ -2211,6 +2267,7 @@ mod tests {
|
||||
anchor_style: None,
|
||||
cwd: ".".to_owned(),
|
||||
file_path: "test.rs".to_owned(),
|
||||
normalize_indent: None,
|
||||
});
|
||||
|
||||
assert!(result.is_err(), "expected error for not-found find text");
|
||||
@@ -2243,6 +2300,7 @@ mod tests {
|
||||
anchor_style: None,
|
||||
cwd: ".".to_owned(),
|
||||
file_path: "test.rs".to_owned(),
|
||||
normalize_indent: None,
|
||||
});
|
||||
|
||||
assert!(result.is_err(), "expected error for ambiguous find text");
|
||||
@@ -2271,6 +2329,7 @@ mod tests {
|
||||
anchor_style: None,
|
||||
cwd: ".".to_owned(),
|
||||
file_path: "test.rs".to_owned(),
|
||||
normalize_indent: None,
|
||||
});
|
||||
|
||||
assert!(result.is_err(), "expected error for empty find text");
|
||||
@@ -2299,6 +2358,7 @@ mod tests {
|
||||
anchor_style: None,
|
||||
cwd: ".".to_owned(),
|
||||
file_path: "test.rs".to_owned(),
|
||||
normalize_indent: None,
|
||||
});
|
||||
|
||||
assert!(result.is_err(), "find outside target chunk should fail");
|
||||
@@ -2330,6 +2390,7 @@ mod tests {
|
||||
anchor_style: Some(ChunkAnchorStyle::Full),
|
||||
cwd: ".".to_owned(),
|
||||
file_path: "test.ts".to_owned(),
|
||||
normalize_indent: None,
|
||||
})
|
||||
.expect("edit should apply");
|
||||
|
||||
@@ -2665,6 +2726,7 @@ mod tests {
|
||||
anchor_style: Some(ChunkAnchorStyle::Full),
|
||||
cwd: ".".to_owned(),
|
||||
file_path: "test.ts".to_owned(),
|
||||
normalize_indent: None,
|
||||
});
|
||||
let err = result.err().expect("should fail with stale CRC");
|
||||
|
||||
@@ -2702,7 +2764,7 @@ mod tests {
|
||||
let new_table = "| Header A | Header B |\n| --- | --- |\n| cell A | cell B |\n";
|
||||
|
||||
// Simulate what normalize_inserted_content does to table content.
|
||||
let result = super::normalize_inserted_content(new_table, "", None, ' ');
|
||||
let result = super::normalize_inserted_content(new_table, "", None, ' ', true);
|
||||
|
||||
assert!(result.contains("| Header A"), "table pipes should not be stripped: {result}");
|
||||
}
|
||||
@@ -3073,6 +3135,7 @@ mod tests {
|
||||
anchor_style: None,
|
||||
cwd: ".".to_owned(),
|
||||
file_path: "test.rs".to_owned(),
|
||||
normalize_indent: None,
|
||||
})
|
||||
.expect("leaf region should fall back to full chunk");
|
||||
|
||||
@@ -3497,6 +3560,7 @@ mod tests {
|
||||
anchor_style: None,
|
||||
cwd: ".".to_owned(),
|
||||
file_path: "test.rs".to_owned(),
|
||||
normalize_indent: None,
|
||||
})
|
||||
.expect("edit should apply");
|
||||
|
||||
@@ -3591,6 +3655,7 @@ mod tests {
|
||||
anchor_style: None,
|
||||
cwd: ".".to_owned(),
|
||||
file_path: "test.rs".to_owned(),
|
||||
normalize_indent: None,
|
||||
})
|
||||
.expect("edit should apply");
|
||||
|
||||
@@ -3780,6 +3845,7 @@ function foo() {\n<<<<<<< HEAD\n\treturn bar();\n=======\n\treturn baz();\n>>>>>
|
||||
anchor_style: None,
|
||||
cwd: ".".to_owned(),
|
||||
file_path: "test.ts".to_owned(),
|
||||
normalize_indent: None,
|
||||
})
|
||||
.expect("batch edit should apply");
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
//! - `classify` — `LangClassifier` trait and dispatch
|
||||
//! - `ast_*` — per-language classifier implementations
|
||||
|
||||
mod atom_list;
|
||||
mod classify;
|
||||
pub(crate) mod common;
|
||||
pub(crate) mod conflict;
|
||||
@@ -16,6 +17,8 @@ pub(crate) mod edit;
|
||||
pub(crate) mod indent;
|
||||
mod render;
|
||||
pub(crate) mod resolve;
|
||||
mod schema;
|
||||
mod shape;
|
||||
pub(crate) mod state;
|
||||
pub mod types;
|
||||
|
||||
@@ -66,7 +69,7 @@ use tree_sitter::{Node, Parser, Tree};
|
||||
use xxhash_rust::xxh64::xxh64;
|
||||
|
||||
use self::{
|
||||
classify::{LangClassifier, classifier_for},
|
||||
classify::{LangClassifier, classifier_for, classify_with_tables, structural_overrides},
|
||||
common::*,
|
||||
kind::ChunkKind,
|
||||
};
|
||||
@@ -112,6 +115,7 @@ pub(crate) fn build_chunk_tree(source: &str, language: &str) -> Result<ChunkTree
|
||||
return Ok(build_blank_line_tree(source, language.to_string(), total_lines, root_checksum));
|
||||
};
|
||||
|
||||
let _schema_language = schema::enter_language(chunk_lang.canonical_name());
|
||||
let classifier = classifier_for(normalized_language.as_str());
|
||||
let tree = parse_tree(source, chunk_lang)?;
|
||||
let root = tree.root_node();
|
||||
@@ -416,12 +420,18 @@ pub(crate) fn collect_children_for_context<'tree>(
|
||||
classifier: &dyn LangClassifier,
|
||||
) -> Vec<RawChunkCandidate<'tree>> {
|
||||
let named_children_list = children_for_context(container, context, classifier);
|
||||
let overrides = structural_overrides(classifier);
|
||||
let mut raw = Vec::new();
|
||||
|
||||
for (index, child) in named_children_list.iter().enumerate() {
|
||||
let is_skippable_trivia = (is_trivia(child.kind()) || classifier.is_trivia(child.kind()))
|
||||
&& !classifier.preserve_trivia(child.kind());
|
||||
if is_skippable_trivia || child.is_missing() {
|
||||
let is_error_node = child.is_error() || child.kind() == "ERROR";
|
||||
let is_skippable_trivia =
|
||||
!is_error_node && is_trivia_for_classifier(*child, classifier, overrides);
|
||||
let is_absorbable_attr = !is_error_node
|
||||
&& (is_absorbable_attribute(child.kind())
|
||||
|| overrides.is_absorbable_attr(child.kind())
|
||||
|| classifier.is_absorbable_attr(child.kind()));
|
||||
if is_skippable_trivia || is_absorbable_attr || (child.is_missing() && !is_error_node) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -449,11 +459,8 @@ fn flatten_root_children<'tree>(
|
||||
classifier: &dyn LangClassifier,
|
||||
) -> Vec<Node<'tree>> {
|
||||
let children = named_children(container);
|
||||
if children.len() == 1
|
||||
&& ((is_root_wrapper_kind(children[0].kind())
|
||||
&& !classifier.preserve_root_wrapper(children[0].kind()))
|
||||
|| classifier.is_root_wrapper(children[0].kind()))
|
||||
{
|
||||
let overrides = structural_overrides(classifier);
|
||||
if children.len() == 1 && is_root_wrapper_for_classifier(children[0], classifier, overrides) {
|
||||
return flatten_root_children(children[0], classifier);
|
||||
}
|
||||
// When a root wrapper's only non-trivia child is another wrapper,
|
||||
@@ -462,12 +469,10 @@ fn flatten_root_children<'tree>(
|
||||
if children.len() > 1 {
|
||||
let non_trivia: Vec<_> = children
|
||||
.iter()
|
||||
.filter(|c| !is_trivia(c.kind()) && !classifier.is_trivia(c.kind()))
|
||||
.filter(|child| !is_trivia_for_classifier(**child, classifier, overrides))
|
||||
.collect();
|
||||
if non_trivia.len() == 1
|
||||
&& is_root_wrapper_kind(non_trivia[0].kind())
|
||||
&& !classifier.preserve_root_wrapper(non_trivia[0].kind())
|
||||
&& is_root_wrapper_kind(container.kind())
|
||||
&& is_root_wrapper_for_classifier(*non_trivia[0], classifier, overrides)
|
||||
{
|
||||
return flatten_root_children(*non_trivia[0], classifier);
|
||||
}
|
||||
@@ -487,14 +492,11 @@ fn classify_node<'tree>(
|
||||
|
||||
// Try language-specific classifier first, then fall back to defaults.
|
||||
match context {
|
||||
ChunkContext::Root => classifier
|
||||
.classify_root(node, source)
|
||||
ChunkContext::Root => classify_with_tables(classifier, context, node, source)
|
||||
.unwrap_or_else(|| defaults::classify_root_default(node, source)),
|
||||
ChunkContext::ClassBody => classifier
|
||||
.classify_class(node, source)
|
||||
ChunkContext::ClassBody => classify_with_tables(classifier, context, node, source)
|
||||
.unwrap_or_else(|| defaults::classify_class_default(node, source)),
|
||||
ChunkContext::FunctionBody => classifier
|
||||
.classify_function(node, source)
|
||||
ChunkContext::FunctionBody => classify_with_tables(classifier, context, node, source)
|
||||
.unwrap_or_else(|| defaults::classify_function_default(node, source)),
|
||||
}
|
||||
}
|
||||
@@ -505,12 +507,13 @@ fn attach_leading_trivia<'tree>(
|
||||
index: usize,
|
||||
classifier: &dyn LangClassifier,
|
||||
) {
|
||||
let overrides = structural_overrides(classifier);
|
||||
let mut cursor = index;
|
||||
while cursor > 0 {
|
||||
let prev = named_children_list[cursor - 1];
|
||||
if !is_trivia(prev.kind())
|
||||
if !is_trivia_for_classifier(prev, classifier, overrides)
|
||||
&& !is_absorbable_attribute(prev.kind())
|
||||
&& !classifier.is_trivia(prev.kind())
|
||||
&& !overrides.is_absorbable_attr(prev.kind())
|
||||
&& !classifier.is_absorbable_attr(prev.kind())
|
||||
{
|
||||
break;
|
||||
@@ -530,6 +533,34 @@ fn attach_leading_trivia<'tree>(
|
||||
}
|
||||
}
|
||||
|
||||
fn is_trivia_for_classifier(
|
||||
node: Node<'_>,
|
||||
classifier: &dyn LangClassifier,
|
||||
overrides: classify::StructuralOverrides,
|
||||
) -> bool {
|
||||
let kind = node.kind();
|
||||
((is_trivia_node(node) || classifier.is_trivia(kind))
|
||||
&& !overrides.preserves_trivia(kind)
|
||||
&& !classifier.preserve_trivia(kind))
|
||||
|| (overrides.is_extra_trivia(kind)
|
||||
&& !overrides.preserves_trivia(kind)
|
||||
&& !classifier.preserve_trivia(kind))
|
||||
}
|
||||
|
||||
fn is_root_wrapper_for_classifier(
|
||||
node: Node<'_>,
|
||||
classifier: &dyn LangClassifier,
|
||||
overrides: classify::StructuralOverrides,
|
||||
) -> bool {
|
||||
let kind = node.kind();
|
||||
if overrides.preserves_root_wrapper(kind) || classifier.preserve_root_wrapper(kind) {
|
||||
return false;
|
||||
}
|
||||
overrides.is_extra_root_wrapper(kind)
|
||||
|| classifier.is_root_wrapper(kind)
|
||||
|| is_root_wrapper_node(node)
|
||||
}
|
||||
|
||||
// ── Grouping / deduplication ─────────────────────────────────────────────
|
||||
|
||||
fn group_candidates(candidates: Vec<RawChunkCandidate<'_>>) -> Vec<RawChunkCandidate<'_>> {
|
||||
@@ -672,7 +703,7 @@ const fn is_collapsible_flat_child(candidate: &RawChunkCandidate<'_>) -> bool {
|
||||
// ── Utility ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn count_parse_errors(node: Node<'_>) -> usize {
|
||||
let mut count = usize::from(node.is_error() || node.is_missing());
|
||||
let mut count = usize::from(node.is_error() || node.is_missing() || node.kind() == "ERROR");
|
||||
for child in named_children(node) {
|
||||
count += count_parse_errors(child);
|
||||
}
|
||||
@@ -1195,7 +1226,13 @@ function main(): void {{
|
||||
tree
|
||||
.chunks
|
||||
.iter()
|
||||
.any(|chunk| chunk.kind == ChunkKind::Error && chunk.identifier.is_none())
|
||||
.any(|chunk| chunk.kind == ChunkKind::Error && chunk.identifier.is_none()),
|
||||
"expected error chunk, got {:?}",
|
||||
tree
|
||||
.chunks
|
||||
.iter()
|
||||
.map(|chunk| (&chunk.path, chunk.kind, chunk.identifier.as_deref()))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2749,7 +2786,7 @@ end
|
||||
fn nix_let_expression_bindings_are_individually_addressable() {
|
||||
// A Nix file with { args }: let bindings in body should produce
|
||||
// individual chunks for each binding, not a single opaque chunk.
|
||||
let source = r#"{ pkgs }:
|
||||
let source = r"{ pkgs }:
|
||||
let
|
||||
foo = 1;
|
||||
bar = pkgs.hello;
|
||||
@@ -2761,7 +2798,7 @@ end
|
||||
{
|
||||
inherit foo bar baz;
|
||||
}
|
||||
"#;
|
||||
";
|
||||
let tree = build_chunk_tree(source, "nix").expect("nix tree should build");
|
||||
|
||||
// There should be chunks for individual bindings, not just a single
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
use std::{cell::RefCell, collections::HashMap};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GeneratedSchema {
|
||||
languages: HashMap<String, HashMap<String, NodeTypeSchema>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct NodeTypeSchema {
|
||||
pub identifier_fields: Vec<String>,
|
||||
pub body_fields: Vec<String>,
|
||||
pub container_child_kinds: Vec<String>,
|
||||
pub is_supertype: bool,
|
||||
pub has_structural_children: bool,
|
||||
}
|
||||
|
||||
impl NodeTypeSchema {
|
||||
pub const fn is_structural(&self) -> bool {
|
||||
self.is_supertype
|
||||
|| !self.identifier_fields.is_empty()
|
||||
|| !self.body_fields.is_empty()
|
||||
|| !self.container_child_kinds.is_empty()
|
||||
|| self.has_structural_children
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static CURRENT_LANGUAGE: RefCell<Option<&'static str>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
static GENERATED_SCHEMA: std::sync::LazyLock<HashMap<String, HashMap<String, NodeTypeSchema>>> =
|
||||
std::sync::LazyLock::new(|| {
|
||||
let raw = include_str!(concat!(env!("OUT_DIR"), "/chunk_schema.json"));
|
||||
let generated: GeneratedSchema =
|
||||
serde_json::from_str(raw).expect("generated chunk schema should parse");
|
||||
generated.languages
|
||||
});
|
||||
|
||||
pub struct SchemaLanguageGuard {
|
||||
previous: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl Drop for SchemaLanguageGuard {
|
||||
fn drop(&mut self) {
|
||||
CURRENT_LANGUAGE.with(|current| {
|
||||
*current.borrow_mut() = self.previous;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enter_language(language: &'static str) -> SchemaLanguageGuard {
|
||||
let previous = CURRENT_LANGUAGE.with(|current| current.replace(Some(language)));
|
||||
SchemaLanguageGuard { previous }
|
||||
}
|
||||
|
||||
pub fn current_language() -> Option<&'static str> {
|
||||
CURRENT_LANGUAGE.with(|current| *current.borrow())
|
||||
}
|
||||
|
||||
pub fn schema_for(language: &str, kind: &str) -> Option<&'static NodeTypeSchema> {
|
||||
GENERATED_SCHEMA
|
||||
.get(language)
|
||||
.and_then(|schemas| schemas.get(kind))
|
||||
}
|
||||
|
||||
pub fn schema_for_current(kind: &str) -> Option<&'static NodeTypeSchema> {
|
||||
current_language().and_then(|language| schema_for(language, kind))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn has_schema(language: &str) -> bool {
|
||||
GENERATED_SCHEMA.contains_key(language)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{has_schema, schema_for};
|
||||
|
||||
#[test]
|
||||
fn python_function_definition_schema_has_name_and_body() {
|
||||
let schema = schema_for("python", "function_definition")
|
||||
.expect("python function_definition schema should exist");
|
||||
assert_eq!(schema.identifier_fields, vec!["name".to_string()]);
|
||||
assert_eq!(schema.body_fields, vec!["body".to_string()]);
|
||||
assert!(schema.is_structural());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nix_let_expression_schema_exposes_binding_set_child() {
|
||||
let schema =
|
||||
schema_for("nix", "let_expression").expect("nix let_expression schema should exist");
|
||||
assert_eq!(schema.body_fields, vec!["body".to_string()]);
|
||||
assert!(
|
||||
schema
|
||||
.container_child_kinds
|
||||
.iter()
|
||||
.any(|kind| kind == "binding_set"),
|
||||
"let_expression should surface binding_set children"
|
||||
);
|
||||
assert!(schema.has_structural_children);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_schema_covers_expected_languages() {
|
||||
for language in ["python", "nix", "toml", "typescript", "rust", "yaml"] {
|
||||
assert!(has_schema(language), "{language} should have generated schema data");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::{atom_list, schema};
|
||||
|
||||
const IDENTIFIER_FIELD_PRIORITY: &[&str] = &[
|
||||
"name",
|
||||
"identifier",
|
||||
"attrpath",
|
||||
"key",
|
||||
"label",
|
||||
"alias",
|
||||
"field",
|
||||
"member",
|
||||
"property",
|
||||
"tag",
|
||||
"target",
|
||||
"variable",
|
||||
];
|
||||
|
||||
const BODY_FIELD_PRIORITY: &[&str] = &["body", "value", "declaration_list", "block", "members"];
|
||||
|
||||
pub fn signature_end_byte(node: Node<'_>) -> Option<usize> {
|
||||
recurse_target(node)
|
||||
.filter(|child| child.start_byte() > node.start_byte())
|
||||
.map(|child| child.start_byte())
|
||||
}
|
||||
|
||||
pub fn identifier_node(node: Node<'_>) -> Option<Node<'_>> {
|
||||
schema_identifier_node(node)
|
||||
.or_else(|| probe_field_child(node, IDENTIFIER_FIELD_PRIORITY))
|
||||
.or_else(|| {
|
||||
local_named_children(node)
|
||||
.into_iter()
|
||||
.find(|child| looks_like_identifier_kind(child.kind()))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn recurse_target(node: Node<'_>) -> Option<Node<'_>> {
|
||||
schema_body_child(node)
|
||||
.or_else(|| schema_container_child(node))
|
||||
.or_else(|| probe_field_child(node, BODY_FIELD_PRIORITY))
|
||||
.or_else(|| probe_structural_child(node))
|
||||
.and_then(descend_to_structural_target)
|
||||
}
|
||||
|
||||
pub fn value_container_target(node: Node<'_>) -> Option<Node<'_>> {
|
||||
for field in ["value", "body"] {
|
||||
if let Some(child) = node.child_by_field_name(field)
|
||||
&& let Some(target) = descend_to_structural_target(child)
|
||||
{
|
||||
return Some(target);
|
||||
}
|
||||
}
|
||||
|
||||
recurse_target(node)
|
||||
}
|
||||
|
||||
pub fn is_root_wrapper_node(node: Node<'_>) -> bool {
|
||||
if is_atom_node(node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(schema) = schema::schema_for_current(node.kind()) {
|
||||
if schema.is_supertype {
|
||||
return true;
|
||||
}
|
||||
if schema.is_structural() && !is_transparent_recurse_wrapper(node) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if identifier_node(node).is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let non_trivia = local_named_children(node)
|
||||
.into_iter()
|
||||
.filter(|child| !is_generic_trivia(*child))
|
||||
.collect::<Vec<_>>();
|
||||
non_trivia.len() == 1 && node_looks_structural(non_trivia[0])
|
||||
}
|
||||
|
||||
pub fn is_generic_trivia(node: Node<'_>) -> bool {
|
||||
node.is_extra() || kind_looks_like_comment(node.kind())
|
||||
}
|
||||
|
||||
pub fn is_generic_trivia_name(kind: &str) -> bool {
|
||||
kind_looks_like_comment(kind)
|
||||
}
|
||||
|
||||
pub fn is_generic_absorbable_attr(kind: &str) -> bool {
|
||||
matches!(kind, "attribute_item" | "inner_attribute_item")
|
||||
}
|
||||
|
||||
pub fn is_atom_node(node: Node<'_>) -> bool {
|
||||
atom_list::is_atom_node_current(node.kind())
|
||||
}
|
||||
|
||||
fn schema_identifier_node(node: Node<'_>) -> Option<Node<'_>> {
|
||||
let schema = schema::schema_for_current(node.kind())?;
|
||||
for field in &schema.identifier_fields {
|
||||
if let Some(child) = node.child_by_field_name(field) {
|
||||
return Some(child);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn schema_body_child(node: Node<'_>) -> Option<Node<'_>> {
|
||||
let schema = schema::schema_for_current(node.kind())?;
|
||||
for field in &schema.body_fields {
|
||||
if let Some(child) = node.child_by_field_name(field) {
|
||||
return Some(child);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn schema_container_child(node: Node<'_>) -> Option<Node<'_>> {
|
||||
let schema = schema::schema_for_current(node.kind())?;
|
||||
local_named_children(node).into_iter().find(|child| {
|
||||
schema
|
||||
.container_child_kinds
|
||||
.iter()
|
||||
.any(|kind| child.kind() == kind)
|
||||
})
|
||||
}
|
||||
|
||||
fn probe_field_child<'tree>(node: Node<'tree>, fields: &[&str]) -> Option<Node<'tree>> {
|
||||
fields
|
||||
.iter()
|
||||
.find_map(|field| node.child_by_field_name(field))
|
||||
}
|
||||
|
||||
fn probe_structural_child(node: Node<'_>) -> Option<Node<'_>> {
|
||||
local_named_children(node).into_iter().find(|child| {
|
||||
!is_generic_trivia(*child)
|
||||
&& !looks_like_identifier_kind(child.kind())
|
||||
&& !is_atom_node(*child)
|
||||
&& node_looks_structural(*child)
|
||||
})
|
||||
}
|
||||
|
||||
fn descend_to_structural_target(node: Node<'_>) -> Option<Node<'_>> {
|
||||
if is_atom_node(node) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if is_transparent_recurse_wrapper(node) {
|
||||
let child = local_named_children(node)
|
||||
.into_iter()
|
||||
.find(|child| !is_generic_trivia(*child) && node_looks_structural(*child))?;
|
||||
return descend_to_structural_target(child).or(Some(child));
|
||||
}
|
||||
|
||||
if node_looks_structural(node) {
|
||||
return Some(node);
|
||||
}
|
||||
|
||||
probe_structural_child(node).and_then(descend_to_structural_target)
|
||||
}
|
||||
|
||||
fn is_transparent_recurse_wrapper(node: Node<'_>) -> bool {
|
||||
if is_atom_node(node) || identifier_node(node).is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
schema::schema_for_current(node.kind()).is_some_and(|schema| schema.is_supertype)
|
||||
|| matches!(node.kind(), "document" | "stream")
|
||||
|| node.kind().ends_with("_node")
|
||||
}
|
||||
|
||||
fn node_looks_structural(node: Node<'_>) -> bool {
|
||||
if is_atom_node(node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if schema::schema_for_current(node.kind()).is_some_and(|schema| schema.is_structural()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let non_trivia_children = local_named_children(node)
|
||||
.into_iter()
|
||||
.filter(|child| !is_generic_trivia(*child))
|
||||
.collect::<Vec<_>>();
|
||||
!non_trivia_children.is_empty()
|
||||
&& non_trivia_children
|
||||
.iter()
|
||||
.any(|child| !looks_like_identifier_kind(child.kind()) || child.named_child_count() > 0)
|
||||
}
|
||||
|
||||
fn local_named_children(node: Node<'_>) -> Vec<Node<'_>> {
|
||||
let mut children = Vec::new();
|
||||
for index in 0..node.child_count() {
|
||||
if let Some(child) = node.child(index)
|
||||
&& (child.is_named() || child.is_error() || child.kind() == "ERROR")
|
||||
{
|
||||
children.push(child);
|
||||
}
|
||||
}
|
||||
children
|
||||
}
|
||||
|
||||
fn looks_like_identifier_kind(kind: &str) -> bool {
|
||||
kind == "identifier"
|
||||
|| kind == "name"
|
||||
|| kind.ends_with("_identifier")
|
||||
|| kind.ends_with("_name")
|
||||
|| kind.ends_with("_label")
|
||||
}
|
||||
|
||||
fn kind_looks_like_comment(kind: &str) -> bool {
|
||||
kind == "comment" || kind.contains("comment")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ast_grep_core::tree_sitter::LanguageExt;
|
||||
use tree_sitter::{Node, Parser};
|
||||
|
||||
use super::{identifier_node, is_root_wrapper_node, recurse_target, signature_end_byte};
|
||||
use crate::language::SupportLang;
|
||||
|
||||
fn parse_tree_with_language(
|
||||
source: &str,
|
||||
language: SupportLang,
|
||||
) -> (crate::chunk::schema::SchemaLanguageGuard, tree_sitter::Tree) {
|
||||
let schema_language = crate::chunk::schema::enter_language(language.canonical_name());
|
||||
let mut parser = Parser::new();
|
||||
parser
|
||||
.set_language(&language.get_ts_language())
|
||||
.expect("language should parse");
|
||||
let tree = parser.parse(source, None).expect("tree should parse");
|
||||
(schema_language, tree)
|
||||
}
|
||||
|
||||
fn find_named_node<'tree>(node: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
|
||||
if node.kind() == kind {
|
||||
return Some(node);
|
||||
}
|
||||
for index in 0..node.child_count() {
|
||||
let Some(child) = node.child(index) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(found) = find_named_node(child, kind) {
|
||||
return Some(found);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_function_definition_resolves_identifier_and_body() {
|
||||
let (_schema_language, tree) =
|
||||
parse_tree_with_language("def greet(name):\n return name\n", SupportLang::Python);
|
||||
let node =
|
||||
find_named_node(tree.root_node(), "function_definition").expect("function_definition");
|
||||
assert_eq!(identifier_node(node).expect("name").kind(), "identifier");
|
||||
assert_eq!(recurse_target(node).expect("body").kind(), "block");
|
||||
assert!(signature_end_byte(node).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typescript_class_declaration_resolves_body() {
|
||||
let (_schema_language, tree) =
|
||||
parse_tree_with_language("class Greeter {\n hello() {}\n}\n", SupportLang::TypeScript);
|
||||
let node = find_named_node(tree.root_node(), "class_declaration").expect("class_declaration");
|
||||
assert_eq!(recurse_target(node).expect("body").kind(), "class_body");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn yaml_wrapper_nodes_are_structural_without_shared_kind_lists() {
|
||||
let (_schema_language, tree) =
|
||||
parse_tree_with_language("root:\n child: 1\n", SupportLang::Yaml);
|
||||
let node = find_named_node(tree.root_node(), "block_node").expect("block_node");
|
||||
assert!(is_root_wrapper_node(node), "block_node should be treated as a structural wrapper");
|
||||
}
|
||||
}
|
||||
@@ -359,6 +359,9 @@ pub struct EditOperation {
|
||||
pub struct EditParams {
|
||||
/// Edits to apply in order.
|
||||
pub operations: Vec<EditOperation>,
|
||||
/// When true, normalize indentation for response rendering and inserted
|
||||
/// content. When false, preserve literal tabs/spaces.
|
||||
pub normalize_indent: Option<bool>,
|
||||
/// Default chunk selector when an `EditOperation` omits `sel`.
|
||||
pub default_selector: Option<String>,
|
||||
/// Default checksum when an `EditOperation` omits `crc`.
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
type ToolConfiguration,
|
||||
ToolResultStatus,
|
||||
} from "@aws-sdk/client-bedrock-runtime";
|
||||
import { $env } from "@oh-my-pi/pi-utils";
|
||||
import { $env, $flag } from "@oh-my-pi/pi-utils";
|
||||
import { NodeHttpHandler } from "@smithy/node-http-handler";
|
||||
import type { Effort } from "../model-thinking";
|
||||
import { mapEffortToAnthropicAdaptiveEffort, requireSupportedEffort } from "../model-thinking";
|
||||
@@ -102,14 +102,14 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
||||
config.region = config.region || $env.AWS_REGION || $env.AWS_DEFAULT_REGION;
|
||||
|
||||
// Support proxies that don't need authentication
|
||||
if ($env.AWS_BEDROCK_SKIP_AUTH === "1") {
|
||||
if ($flag("AWS_BEDROCK_SKIP_AUTH")) {
|
||||
config.credentials = {
|
||||
accessKeyId: "dummy-access-key",
|
||||
secretAccessKey: "dummy-secret-key",
|
||||
};
|
||||
}
|
||||
|
||||
if ($env.AWS_BEDROCK_FORCE_HTTP1 === "1") {
|
||||
if ($flag("AWS_BEDROCK_FORCE_HTTP1")) {
|
||||
config.requestHandler = new NodeHttpHandler();
|
||||
}
|
||||
}
|
||||
@@ -379,7 +379,7 @@ function supportsPromptCaching(model: Model<"bedrock-converse-stream">): boolean
|
||||
if (id.includes("claude-haiku")) return true;
|
||||
// Application inference profiles don't contain the model name in the ARN.
|
||||
// Allow users to force cache points via environment variable.
|
||||
if (typeof process !== "undefined" && process.env.AWS_BEDROCK_FORCE_CACHE === "1") return true;
|
||||
if (typeof process !== "undefined" && $flag("AWS_BEDROCK_FORCE_CACHE")) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as os from "node:os";
|
||||
import { $env, abortableSleep, asRecord, logger, readSseJson, structuredCloneJSON } from "@oh-my-pi/pi-utils";
|
||||
import { $env, $flag, abortableSleep, asRecord, logger, readSseJson, structuredCloneJSON } from "@oh-my-pi/pi-utils";
|
||||
import type OpenAI from "openai";
|
||||
import type {
|
||||
ResponseFunctionToolCall,
|
||||
@@ -90,7 +90,7 @@ export function buildCodexSystemPrompt(args: { userSystemPrompt?: string }): Cod
|
||||
};
|
||||
}
|
||||
|
||||
const CODEX_DEBUG = $env.PI_CODEX_DEBUG === "1" || $env.PI_CODEX_DEBUG === "true";
|
||||
const CODEX_DEBUG = $flag("PI_CODEX_DEBUG");
|
||||
const CODEX_MAX_RETRIES = 5;
|
||||
const CODEX_RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);
|
||||
const CODEX_RETRY_DELAY_MS = 500;
|
||||
@@ -197,7 +197,7 @@ function parseCodexPositiveInteger(value: string | undefined, fallback: number):
|
||||
}
|
||||
|
||||
function isCodexWebSocketEnvEnabled(): boolean {
|
||||
return $env.PI_CODEX_WEBSOCKET === "1" || $env.PI_CODEX_WEBSOCKET === "true";
|
||||
return $flag("PI_CODEX_WEBSOCKET");
|
||||
}
|
||||
|
||||
function getCodexWebSocketRetryBudget(): number {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { $flag } from "@oh-my-pi/pi-utils";
|
||||
import { type TUnsafe, Type } from "@sinclair/typebox";
|
||||
import { areJsonValuesEqual } from "./equality";
|
||||
import { COMBINATOR_KEYS, NON_STRUCTURAL_SCHEMA_KEYS } from "./fields";
|
||||
@@ -26,7 +27,7 @@ export function StringEnum<const T extends readonly string[]>(
|
||||
});
|
||||
}
|
||||
|
||||
export const NO_STRICT = Bun.env.PI_NO_STRICT === "1";
|
||||
export const NO_STRICT = $flag("PI_NO_STRICT");
|
||||
|
||||
const strictSchemaCache = new WeakMap<Record<string, unknown>, { schema: Record<string, unknown>; strict: boolean }>();
|
||||
function hasUnrepresentableStrictObjectMap(schema: Record<string, unknown>, seen?: WeakSet<object>): boolean {
|
||||
|
||||
@@ -10,13 +10,13 @@ import * as fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { getOAuthApiKey } from "@oh-my-pi/pi-ai/utils/oauth";
|
||||
import type { OAuthCredentials, OAuthProvider } from "@oh-my-pi/pi-ai/utils/oauth/types";
|
||||
import { getAgentDir, isEnoent } from "@oh-my-pi/pi-utils";
|
||||
import { $flag, getAgentDir, isEnoent } from "@oh-my-pi/pi-utils";
|
||||
|
||||
/**
|
||||
* E2E tests require explicit opt-in via E2E=1 environment variable.
|
||||
* This prevents accidental API calls when keys happen to be in the environment.
|
||||
*/
|
||||
export const E2E_ENABLED = Bun.env.E2E === "1" || Bun.env.E2E === "true";
|
||||
export const E2E_ENABLED = $flag("E2E");
|
||||
|
||||
/**
|
||||
* Get an API key from environment, but only if E2E tests are enabled.
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
|
||||
- Added `PI_CHUNK_AUTOINDENT` environment variable to control whether chunk read/edit tools normalize indentation to canonical tabs or preserve literal file whitespace
|
||||
- Added dynamic chunk tool prompts that automatically adjust guidance based on `PI_CHUNK_AUTOINDENT` setting without exposing a tool parameter
|
||||
- Added `<instruction-priority>`, `<output-contract>`, `<default-follow-through>`, `<tool-persistence>`, and `<completeness-contract>` sections to system prompt for improved long-horizon agent workflows
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated chunk edit tool to apply `normalizeIndent` setting during edit operations, enabling literal whitespace preservation when `PI_CHUNK_AUTOINDENT=0`
|
||||
- Refactored environment variable parsing to use `$flag()` and `$envpos()` utilities from pi-utils for consistent boolean and integer handling across codebase
|
||||
- Updated system prompt communication guidelines to emphasize conciseness and information density, and added guidance on avoiding repetition of user requests
|
||||
- Enhanced system prompt with explicit rules for design integrity, verification before yielding, and handling of missing context via tool-based retrieval
|
||||
- Added `PI_CHUNK_AUTOINDENT` to control whether chunk read/edit tools normalize indentation, and updated chunk prompts to switch guidance automatically based on that setting
|
||||
- Refined the default system prompt with explicit instruction-priority, output-contract, tool-persistence, completeness, and verification rules for long-horizon GPT-5.4-style agent workflows
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed typo in system prompt: 'backwards compatibiltity' → 'backwards compatibility'
|
||||
|
||||
## [14.0.3] - 2026-04-09
|
||||
|
||||
@@ -6900,4 +6918,4 @@ Initial public release.
|
||||
- Git branch display in footer
|
||||
- Message queueing during streaming responses
|
||||
- OAuth integration for Gmail and Google Calendar access
|
||||
- HTML export with syntax highlighting and collapsible sections
|
||||
- HTML export with syntax highlighting and collapsible sections
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
executeChunkMode,
|
||||
isChunkParams,
|
||||
resolveAnchorStyle,
|
||||
resolveChunkAutoIndent,
|
||||
} from "./modes/chunk";
|
||||
import { executeHashlineMode, type HashlineParams, hashlineEditParamsSchema, isHashlineParams } from "./modes/hashline";
|
||||
import { executePatchMode, isPatchParams, type PatchParams, patchEditSchema } from "./modes/patch";
|
||||
@@ -197,6 +198,7 @@ export class EditTool implements AgentTool<TInput> {
|
||||
description: (session: ToolSession) =>
|
||||
prompt.render(chunkEditDescription, {
|
||||
anchorStyle: resolveAnchorStyle(session.settings),
|
||||
chunkAutoIndent: resolveChunkAutoIndent(),
|
||||
}),
|
||||
parameters: chunkEditParamsSchema,
|
||||
invalidParamsMessage: "Invalid edit parameters for chunk mode.",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ChunkState,
|
||||
type EditOperation as NativeEditOperation,
|
||||
} from "@oh-my-pi/pi-natives";
|
||||
import { $envpos } from "@oh-my-pi/pi-utils";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import type { BunFile } from "bun";
|
||||
import { LRUCache } from "lru-cache";
|
||||
@@ -63,6 +64,34 @@ const validAnchorStyles: Record<string, ChunkAnchorStyle> = {
|
||||
bare: ChunkAnchorStyle.Bare,
|
||||
};
|
||||
|
||||
export function resolveChunkAutoIndent(rawValue = Bun.env.PI_CHUNK_AUTOINDENT): boolean {
|
||||
if (!rawValue) return true;
|
||||
const normalized = rawValue.trim().toLowerCase();
|
||||
switch (normalized) {
|
||||
case "1":
|
||||
case "true":
|
||||
case "yes":
|
||||
case "on":
|
||||
return true;
|
||||
case "0":
|
||||
case "false":
|
||||
case "no":
|
||||
case "off":
|
||||
return false;
|
||||
default:
|
||||
throw new Error(`Invalid PI_CHUNK_AUTOINDENT: ${rawValue}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getChunkRenderIndentOptions(): {
|
||||
normalizeIndent: boolean;
|
||||
tabReplacement: string;
|
||||
} {
|
||||
return resolveChunkAutoIndent()
|
||||
? { normalizeIndent: true, tabReplacement: " " }
|
||||
: { normalizeIndent: false, tabReplacement: "\t" };
|
||||
}
|
||||
|
||||
export function resolveAnchorStyle(settings?: Settings): ChunkAnchorStyle {
|
||||
const envStyle = Bun.env.PI_ANCHOR_STYLE;
|
||||
return (
|
||||
@@ -72,16 +101,8 @@ export function resolveAnchorStyle(settings?: Settings): ChunkAnchorStyle {
|
||||
);
|
||||
}
|
||||
|
||||
const readEnvInt = (name: string, defaultValue: number): number => {
|
||||
const value = Bun.env[name];
|
||||
if (!value) return defaultValue;
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (Number.isNaN(parsed) || parsed <= 0) return defaultValue;
|
||||
return parsed;
|
||||
};
|
||||
|
||||
const chunkStateCache = new LRUCache<string, ChunkCacheEntry>({
|
||||
max: readEnvInt("PI_CHUNK_CACHE_MAX_ENTRIES", 200),
|
||||
max: $envpos("PI_CHUNK_CACHE_MAX_ENTRIES", 200),
|
||||
});
|
||||
|
||||
export function invalidateChunkCache(filePath: string): void {
|
||||
@@ -215,6 +236,7 @@ export async function formatChunkedRead(params: {
|
||||
const normalizedLanguage = normalizeLanguage(language);
|
||||
const { state } = await loadChunkStateForFile(filePath, normalizedLanguage);
|
||||
const displayPath = displayPathForFile(filePath, cwd);
|
||||
const renderIndentOptions = getChunkRenderIndentOptions();
|
||||
const result = state.renderRead({
|
||||
readPath,
|
||||
displayPath,
|
||||
@@ -224,8 +246,8 @@ export async function formatChunkedRead(params: {
|
||||
absoluteLineRange: absoluteLineRange
|
||||
? { startLine: absoluteLineRange.startLine, endLine: absoluteLineRange.endLine ?? absoluteLineRange.startLine }
|
||||
: undefined,
|
||||
tabReplacement: " ",
|
||||
normalizeIndent: true,
|
||||
tabReplacement: renderIndentOptions.tabReplacement,
|
||||
normalizeIndent: renderIndentOptions.normalizeIndent,
|
||||
});
|
||||
return { text: result.text, resolvedPath: filePath, chunk: result.chunk };
|
||||
}
|
||||
@@ -280,6 +302,7 @@ export function applyChunkEdits(params: {
|
||||
const state = ChunkState.parse(normalizedSource, normalizeLanguage(params.language));
|
||||
const result = state.applyEdits({
|
||||
operations: nativeOperations,
|
||||
normalizeIndent: resolveChunkAutoIndent(),
|
||||
defaultSelector: params.defaultSelector,
|
||||
defaultCrc: params.defaultCrc,
|
||||
anchorStyle: params.anchorStyle,
|
||||
@@ -312,7 +335,8 @@ export const chunkToolEditSchema = Type.Object({
|
||||
"Chunk selector. Format: 'path@region' for insertions, 'path#CRC@region' for replace. Omit @region to target the full chunk. Valid regions: head, body, tail, decl.",
|
||||
}),
|
||||
content: Type.String({
|
||||
description: "New content. Use \\t for indentation. Do NOT include the chunk's base padding.",
|
||||
description:
|
||||
"New content. Write indentation relative to the targeted region as described in the tool prompt. Do NOT include the chunk's base padding.",
|
||||
}),
|
||||
});
|
||||
export const chunkEditParamsSchema = Type.Object(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as path from "node:path";
|
||||
import { getAgentDir, getProjectDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
||||
import { getAgentDir, getProjectDir, isBunTestRuntime, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
||||
import { OutputSink } from "../session/streaming-output";
|
||||
import { shutdownSharedGateway } from "./gateway-coordinator";
|
||||
import {
|
||||
@@ -299,10 +299,6 @@ async function writePreludeCache(state: PreludeCacheState, helpers: PreludeHelpe
|
||||
}
|
||||
}
|
||||
|
||||
function isPythonTestEnvironment(): boolean {
|
||||
return Bun.env.BUN_ENV === "test" || Bun.env.NODE_ENV === "test";
|
||||
}
|
||||
|
||||
function getPreludeIntrospectionOptions(
|
||||
options: KernelSessionExecutionOptions = {},
|
||||
): Pick<KernelExecuteOptions, "signal" | "timeoutMs"> {
|
||||
@@ -318,7 +314,7 @@ async function cachePreludeDocs(
|
||||
cacheState?: PreludeCacheState | null,
|
||||
): Promise<PreludeHelper[]> {
|
||||
cachedPreludeDocs = docs;
|
||||
if (!isPythonTestEnvironment() && docs.length > 0) {
|
||||
if (!isBunTestRuntime() && docs.length > 0) {
|
||||
const state = cacheState ?? (await buildPreludeCacheState(cwd));
|
||||
await writePreludeCache(state, docs);
|
||||
}
|
||||
@@ -416,7 +412,7 @@ export async function warmPythonEnvironment(
|
||||
cachedPreludeDocs = [];
|
||||
return { ok: false, reason, docs: [] };
|
||||
}
|
||||
if (!isPythonTestEnvironment()) {
|
||||
if (!isBunTestRuntime()) {
|
||||
try {
|
||||
cacheState = await buildPreludeCacheState(cwd);
|
||||
const cached = await readPreludeCache(cacheState);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { $env, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
||||
import { $env, $flag, isBunTestRuntime, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
||||
import { $ } from "bun";
|
||||
import { Settings } from "../config/settings";
|
||||
import { htmlToBasicMarkdown } from "../web/scrapers/types";
|
||||
@@ -10,7 +10,7 @@ import { filterEnv, resolvePythonRuntime } from "./runtime";
|
||||
|
||||
const TEXT_ENCODER = new TextEncoder();
|
||||
const TEXT_DECODER = new TextDecoder();
|
||||
const TRACE_IPC = $env.PI_PYTHON_IPC_TRACE === "1";
|
||||
const TRACE_IPC = $flag("PI_PYTHON_IPC_TRACE");
|
||||
const PRELUDE_INTROSPECTION_SNIPPET = "import json\nprint(json.dumps(__omp_prelude_docs__()))";
|
||||
|
||||
class SharedGatewayCreateError extends Error {
|
||||
@@ -195,7 +195,7 @@ export interface PythonKernelAvailability {
|
||||
}
|
||||
|
||||
export async function checkPythonKernelAvailability(cwd: string): Promise<PythonKernelAvailability> {
|
||||
if (Bun.env.BUN_ENV === "test" || Bun.env.NODE_ENV === "test" || $env.PI_PYTHON_SKIP_CHECK === "1") {
|
||||
if (isBunTestRuntime() || $flag("PI_PYTHON_SKIP_CHECK")) {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { $env, $which, logger } from "@oh-my-pi/pi-utils";
|
||||
import { $flag, $which, logger } from "@oh-my-pi/pi-utils";
|
||||
import { TOML } from "bun";
|
||||
|
||||
/**
|
||||
@@ -135,7 +135,7 @@ export async function detectLspmux(): Promise<LspmuxState> {
|
||||
return cachedState;
|
||||
}
|
||||
|
||||
if ($env.PI_DISABLE_LSPMUX === "1") {
|
||||
if ($flag("PI_DISABLE_LSPMUX")) {
|
||||
cachedState = { available: false, running: false, binaryPath: null, config: null };
|
||||
cacheTimestamp = now;
|
||||
return cachedState;
|
||||
|
||||
@@ -52,9 +52,29 @@ Push back when warranted: state the downside, propose an alternative, but **MUST
|
||||
<communication>
|
||||
- No emojis, filler, or ceremony.
|
||||
- (1) Correctness first, (2) Brevity second, (3) Politeness third.
|
||||
- User-supplied content **MUST** override any other guidelines.
|
||||
- Prefer concise, information-dense writing.
|
||||
- Avoid repeating the user's request or narrating routine tool calls.
|
||||
</communication>
|
||||
|
||||
<instruction-priority>
|
||||
- User instructions override default style, tone, formatting, and initiative preferences.
|
||||
- Higher-priority system constraints about safety, permissions, tool boundaries, and task completion do not yield.
|
||||
- If a newer user instruction conflicts with an earlier user instruction, follow the newer one.
|
||||
- Preserve earlier instructions that do not conflict.
|
||||
</instruction-priority>
|
||||
|
||||
<output-contract>
|
||||
- Brief preambles are allowed when they improve orientation, but they **MUST** stay short and **MUST NOT** be treated as completion.
|
||||
- Claims about code, tools, tests, docs, or external sources **MUST** be grounded in what you actually observed. If a statement is an inference, say so.
|
||||
- Apply brevity to prose, not to evidence, verification, or blocking details.
|
||||
</output-contract>
|
||||
|
||||
<default-follow-through>
|
||||
- If the user's intent is clear and the next step is reversible and low-risk, proceed without asking.
|
||||
- Ask only when the next step is irreversible, has external side effects, or requires a missing choice that would materially change the outcome.
|
||||
- If you proceed, state what you did, what you verified, and what remains optional.
|
||||
</default-follow-through>
|
||||
|
||||
<behavior>
|
||||
You **MUST** guard against the completion reflex — the urge to ship something that compiles before you've understood the problem:
|
||||
- Compiling ≠ Correctness. "It works" ≠ "Works in all cases".
|
||||
@@ -248,6 +268,15 @@ Don't open a file hoping. Hope is not a strategy.
|
||||
{{#has tools "task"}}- `task` for investigate+edit in one pass — prefer this over a separate explore→task chain{{/has}}
|
||||
{{/ifAny}}
|
||||
|
||||
<tool-persistence>
|
||||
- Use tools whenever they materially improve correctness, completeness, or grounding.
|
||||
- Do not stop at the first plausible answer if another tool call would materially reduce uncertainty, verify a dependency, or improve coverage.
|
||||
- Before taking an action, check whether prerequisite discovery, lookup, or memory retrieval is required. Resolve prerequisites first.
|
||||
- If a lookup is empty, partial, or suspiciously narrow, retry with a different strategy before concluding nothing exists.
|
||||
- When multiple retrieval steps are independent, parallelize them. When one result determines the next step, keep the workflow sequential.
|
||||
- After parallel retrieval, pause to synthesize before making more calls.
|
||||
</tool-persistence>
|
||||
|
||||
{{#if (includes tools "inspect_image")}}
|
||||
### Image inspection
|
||||
- For image understanding tasks: **MUST** use `inspect_image` over `read` to avoid overloading main session context.
|
||||
@@ -262,7 +291,14 @@ These are inviolable. Violation is system failure.
|
||||
- You **MUST NOT** suppress tests to make code pass. You **MUST NOT** fabricate outputs not observed.
|
||||
- You **MUST NOT** solve the wished-for problem instead of the actual problem.
|
||||
- You **MUST NOT** ask for information obtainable from tools, repo context, or files.
|
||||
- You **MUST** always design a clean solution. You **MUST NOT** introduce unnecessary backwards compatibiltity layers, no shims, no gradual migration, no bridges to old code unless user explicitly asks for it. Let the errors guide you on what to include in the refactoring. **ALWAYS default to performing full CUTOVER!**
|
||||
- You **MUST** always design a clean solution. You **MUST NOT** introduce unnecessary backwards compatibility layers, no shims, no gradual migration, no bridges to old code unless user explicitly asks for it. Let the errors guide you on what to include in the refactoring. **ALWAYS default to performing full CUTOVER!**
|
||||
|
||||
<completeness-contract>
|
||||
- Treat the task as incomplete until every requested deliverable is done or explicitly marked [blocked].
|
||||
- Keep an internal checklist of requested outcomes, implied cleanup, affected callsites, tests, docs, and follow-on edits.
|
||||
- For lists, batches, paginated results, or multi-file migrations, determine expected scope when possible and confirm coverage before yielding.
|
||||
- If something is blocked, label it [blocked], say exactly what is missing, and distinguish it from work that is complete.
|
||||
</completeness-contract>
|
||||
|
||||
# Design Integrity
|
||||
|
||||
@@ -280,6 +316,7 @@ Design integrity means the code tells the truth about what the system currently
|
||||
{{#has tools "task"}}- You **MUST** determine if the task is parallelizable via `task` tool.{{/has}}
|
||||
- If multi-file or imprecisely scoped, you **MUST** write out a step-by-step plan, phased if it warrants, before touching any file.
|
||||
- For new work, you **MUST**: (1) think about architecture, (2) search official docs/papers on best practices, (3) review existing codebase, (4) compare research with codebase, (5) implement the best fit or surface tradeoffs.
|
||||
- If required context is missing, do **NOT** guess. Prefer tool-based retrieval first, ask a minimal question only when the answer cannot be recovered from tools, repo context, or files.
|
||||
## 2. Before You Edit
|
||||
- Read the relevant section of any file before editing. Don't edit from a grep snippet alone — context above and below the match changes what the correct edit is.
|
||||
- You **MUST** grep for existing examples before implementing any pattern, utility, or abstraction. If the codebase already solves it, you **MUST** use that. Inventing a parallel convention is **PROHIBITED**.
|
||||
@@ -313,6 +350,7 @@ When a tool call fails, read the full error before doing anything else. When a f
|
||||
- Test everything rigorously → Future contributor cannot break behavior without failure. Prefer unit/e2e.
|
||||
- You **MUST NOT** rely on mocks — they invent behaviors that never happen in production and hide real bugs.
|
||||
- You **SHOULD** run only tests you added/modified unless asked otherwise.
|
||||
- Before yielding, verify: (1) every requirement is satisfied, (2) claims match files/tool output/source material, (3) the output format matches the ask, and (4) any high-impact action was either verified or explicitly held for permission.
|
||||
- You **MUST NOT** yield without proof when non-trivial work, self-assessment is deceptive: tests, linters, type checks, repro steps… exhaust all external verification.
|
||||
|
||||
{{#if secretsEnabled}}
|
||||
|
||||
@@ -7,11 +7,20 @@ Edits files via syntax-aware chunks. Run `read(path="file.ts")` first. The edit
|
||||
- replacements: `chunk#CRC` or `chunk#CRC@region`
|
||||
- Without a `@region` it defaults to the entire chunk including leading trivia. Valid regions: `head`, `body`, `tail`, `decl`.
|
||||
- If the exact chunk path is unclear, run `read(path="file", sel="?")` and copy a selector from that listing.
|
||||
{{#if chunkAutoIndent}}
|
||||
- Use `\t` for indentation in `content`. Write content at indent-level 0 — the tool re-indents it to match the chunk's position in the file. For example, to replace `@body` of a method, write the body starting at column 0:
|
||||
```
|
||||
content: "if (x) {\n\treturn true;\n}"
|
||||
```
|
||||
The tool adds the correct base indent automatically. Never manually pad with the chunk's own indentation.
|
||||
{{else}}
|
||||
- Match the file's literal tabs/spaces in `content`. Do not convert indentation to canonical `\t`.
|
||||
- Write content at indent-level 0 relative to the target region. For example, to replace `@body` of a method, write:
|
||||
```
|
||||
content: "if (x) {\n return true;\n}"
|
||||
```
|
||||
The tool adds the correct base indent automatically, then preserves the tabs/spaces you used inside the snippet. Never manually pad with the chunk's own indentation.
|
||||
{{/if}}
|
||||
- `@region` only works on container chunks (classes, functions, impl blocks, sections). Do **not** use `@head`/`@body`/`@tail` on leaf chunks (enum variants, fields, single statements) — use the whole chunk instead.
|
||||
- `replace` requires the current CRC. Insertions do not.
|
||||
- **CRCs change after every edit.** Always use the selectors/CRCs from the most recent `read` or edit response. Never reuse a CRC from a previous edit.
|
||||
@@ -113,7 +122,11 @@ Given this `read` output for `example.ts`:
|
||||
|
||||
**Replace a whole chunk** (rename a function):
|
||||
~~~json
|
||||
{{#if chunkAutoIndent}}
|
||||
{ "sel": "fn_createCounter#PQQY", "op": "replace", "content": "function makeCounter(start: number): Counter {\n\tconst c = new Counter();\n\tc.value = start;\n\treturn c;\n}\n" }
|
||||
{{else}}
|
||||
{ "sel": "fn_createCounter#PQQY", "op": "replace", "content": "function makeCounter(start: number): Counter {\n const c = new Counter();\n c.value = start;\n return c;\n}\n" }
|
||||
{{/if}}
|
||||
~~~
|
||||
Result — the entire chunk is rewritten:
|
||||
```
|
||||
@@ -163,7 +176,11 @@ function createCounter(initial: number): Counter {
|
||||
|
||||
**Insert after a chunk** (`after`):
|
||||
~~~json
|
||||
{{#if chunkAutoIndent}}
|
||||
{ "sel": "enum_Status", "op": "after", "content": "\nfunction isActive(s: Status): boolean {\n\treturn s === Status.Active;\n}\n" }
|
||||
{{else}}
|
||||
{ "sel": "enum_Status", "op": "after", "content": "\nfunction isActive(s: Status): boolean {\n return s === Status.Active;\n}\n" }
|
||||
{{/if}}
|
||||
~~~
|
||||
Result — a new function appears after the enum:
|
||||
```
|
||||
@@ -194,7 +211,11 @@ class Counter {
|
||||
|
||||
**Append inside a container** (`@body` + `append`):
|
||||
~~~json
|
||||
{{#if chunkAutoIndent}}
|
||||
{ "sel": "class_Counter@body", "op": "append", "content": "\nreset(): void {\n\tthis.value = 0;\n}\n" }
|
||||
{{else}}
|
||||
{ "sel": "class_Counter@body", "op": "append", "content": "\nreset(): void {\n this.value = 0;\n}\n" }
|
||||
{{/if}}
|
||||
~~~
|
||||
Result — a new method is added at the end of the class body, before the closing `}`:
|
||||
```
|
||||
@@ -214,10 +235,18 @@ Result — a new method is added at the end of the class body, before the closin
|
||||
```
|
||||
Result — the method is removed from the class.
|
||||
- Indentation rules (important):
|
||||
{{#if chunkAutoIndent}}
|
||||
- Use `\t` for each indent level. The tool converts tabs to the file's actual style (2-space, 4-space, etc.).
|
||||
{{else}}
|
||||
- Match the file's real indentation characters in your snippet. The tool preserves your literal tabs/spaces after adding the target region's base indent.
|
||||
{{/if}}
|
||||
- Do NOT include the chunk's base indentation — only indent relative to the region's opening level.
|
||||
- For `@body` of a function: write at column 0, e.g. `"return x;\n"`. The tool adds the correct base indent.
|
||||
- For `@head`: write at the chunk's own depth. A class member's head uses `"/** doc */\nstart(): void {"`.
|
||||
{{#if chunkAutoIndent}}
|
||||
- For a top-level item: start at zero indent. Write `"function foo() {\n\treturn 1;\n}\n"`.
|
||||
{{else}}
|
||||
- For a top-level item: start at zero indent. Write `"function foo() {\n return 1;\n}\n"`.
|
||||
{{/if}}
|
||||
- The tool strips common leading indentation from your content as a safety net, so accidental over-indentation is corrected.
|
||||
</examples>
|
||||
|
||||
@@ -9,6 +9,12 @@ Each opening anchor `[< full.chunk.path#CCCC ]` in the default output identifies
|
||||
If you need a canonical target list, run `read(path="file", sel="?")`. That listing shows chunk paths with CRCs.
|
||||
Line numbers in the gutter are absolute file line numbers.
|
||||
|
||||
{{#if chunkAutoIndent}}
|
||||
Chunk reads normalize leading indentation so copied content round-trips cleanly into chunk edits.
|
||||
{{else}}
|
||||
Chunk reads preserve literal leading tabs/spaces from the file. When editing, keep the same whitespace characters you see here.
|
||||
{{/if}}
|
||||
|
||||
Chunk trees: JS, TS, TSX, Python, Rust, Go. Others use blank-line fallback.
|
||||
</instruction>
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { SearchDb } from "@oh-my-pi/pi-natives";
|
||||
import type { Component } from "@oh-my-pi/pi-tui";
|
||||
import {
|
||||
$env,
|
||||
$flag,
|
||||
getAgentDbPath,
|
||||
getAgentDir,
|
||||
getProjectDir,
|
||||
@@ -1262,7 +1263,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
|
||||
const repeatToolDescriptions = settings.get("repeatToolDescriptions");
|
||||
const eagerTasks = settings.get("task.eager");
|
||||
const intentField = settings.get("tools.intentTracing") || $env.PI_INTENT_TRACING === "1" ? INTENT_FIELD : undefined;
|
||||
const intentField = settings.get("tools.intentTracing") || $flag("PI_INTENT_TRACING") ? INTENT_FIELD : undefined;
|
||||
const rebuildSystemPrompt = async (toolNames: string[], tools: Map<string, AgentTool>): Promise<string> => {
|
||||
toolContextStore.setToolNames(toolNames);
|
||||
const discoverableMCPTools = mcpDiscoveryEnabled ? collectDiscoverableMCPTools(tools.values()) : [];
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
|
||||
import { type AstReplaceChange, astEdit } from "@oh-my-pi/pi-natives";
|
||||
import type { Component } from "@oh-my-pi/pi-tui";
|
||||
import { Text } from "@oh-my-pi/pi-tui";
|
||||
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
||||
import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import { computeLineHash } from "../edit/line-hash";
|
||||
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
||||
@@ -103,7 +103,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
||||
if (maxReplacements !== undefined && (!Number.isFinite(maxReplacements) || maxReplacements < 1)) {
|
||||
throw new ToolError("limit must be a positive number");
|
||||
}
|
||||
const maxFiles = parseInt(process.env.PI_MAX_AST_FILES ?? "", 10) || 1000;
|
||||
const maxFiles = $envpos("PI_MAX_AST_FILES", 1000);
|
||||
|
||||
const formatScopePath = (targetPath: string): string => {
|
||||
const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
||||
import type { ToolChoice } from "@oh-my-pi/pi-ai";
|
||||
import type { SearchDb } from "@oh-my-pi/pi-natives";
|
||||
import { $env, logger } from "@oh-my-pi/pi-utils";
|
||||
import { $env, $flag, isBunTestRuntime, logger } from "@oh-my-pi/pi-utils";
|
||||
import type { AsyncJobManager } from "../async";
|
||||
import type { PromptTemplate } from "../config/prompt-templates";
|
||||
import type { Settings } from "../config/settings";
|
||||
@@ -297,8 +297,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
||||
!skipPythonPreflight &&
|
||||
pythonMode !== "bash-only" &&
|
||||
(requestedTools === undefined || requestedTools.includes("python"));
|
||||
const isTestEnv = Bun.env.BUN_ENV === "test" || Bun.env.NODE_ENV === "test";
|
||||
const skipPythonWarm = isTestEnv || $env.PI_PYTHON_SKIP_CHECK === "1";
|
||||
const skipPythonWarm = isBunTestRuntime() || $flag("PI_PYTHON_SKIP_CHECK");
|
||||
if (shouldCheckPython) {
|
||||
const availability = await logger.time("createTools:pythonCheck", checkPythonKernelAvailability, session.cwd);
|
||||
pythonAvailable = availability.ok;
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
parseChunkReadPath,
|
||||
parseChunkSelector,
|
||||
resolveAnchorStyle,
|
||||
resolveChunkAutoIndent,
|
||||
} from "../edit/modes/chunk";
|
||||
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
||||
import { parseInternalUrl } from "../internal-urls/parse";
|
||||
@@ -449,6 +450,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
||||
resolveEditMode(session) === "chunk"
|
||||
? prompt.render(readChunkDescription, {
|
||||
anchorStyle: resolveAnchorStyle(session.settings),
|
||||
chunkAutoIndent: resolveChunkAutoIndent(),
|
||||
})
|
||||
: prompt.render(readDescription, {
|
||||
DEFAULT_LIMIT: String(this.#defaultLimit),
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
import path from "node:path";
|
||||
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
||||
import { $env, getAgentDir, logger, VERSION } from "@oh-my-pi/pi-utils";
|
||||
import { $flag, getAgentDir, logger, VERSION } from "@oh-my-pi/pi-utils";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { Settings } from "..";
|
||||
import type { ToolSession } from "./index";
|
||||
@@ -19,7 +19,7 @@ const ReportToolIssueParams = Type.Object({
|
||||
});
|
||||
|
||||
export function isAutoQaEnabled(settings?: Settings): boolean {
|
||||
return $env.PI_AUTO_QA === "1" || !!settings?.get("dev.autoqa");
|
||||
return $flag("PI_AUTO_QA") || !!settings?.get("dev.autoqa");
|
||||
}
|
||||
|
||||
export function getAutoQaDbPath(): string {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { $env } from "@oh-my-pi/pi-utils";
|
||||
import { $env, $flag } from "@oh-my-pi/pi-utils";
|
||||
|
||||
export type EditMode = "replace" | "patch" | "hashline" | "chunk";
|
||||
|
||||
@@ -36,7 +36,7 @@ export function resolveEditMode(session: EditModeSessionLike): EditMode {
|
||||
const envMode = normalizeEditMode($env.PI_EDIT_VARIANT);
|
||||
if (envMode) return envMode;
|
||||
|
||||
if ($env.PI_STRICT_EDIT_MODE === "1") {
|
||||
if (!$flag("PI_STRICT_EDIT_MODE")) {
|
||||
if (activeModel?.includes("spark")) return "replace";
|
||||
if (activeModel?.includes("nano")) return "replace";
|
||||
if (activeModel?.includes("mini")) return "replace";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { $env } from "@oh-my-pi/pi-utils";
|
||||
import { $env, $flag } from "@oh-my-pi/pi-utils";
|
||||
|
||||
const SIXEL_START_REGEX = /\x1bP(?:[0-9;]*)q/u;
|
||||
const SIXEL_END_SEQUENCE = "\x1b\\";
|
||||
@@ -15,7 +15,7 @@ const SIXEL_PLACEHOLDER_PREFIX = "__OMP_SIXEL_SEQUENCE_";
|
||||
*/
|
||||
export function isSixelPassthroughEnabled(): boolean {
|
||||
const forcedProtocol = $env.PI_FORCE_IMAGE_PROTOCOL?.trim().toLowerCase();
|
||||
return forcedProtocol === "sixel" && $env.PI_ALLOW_SIXEL_PASSTHROUGH === "1";
|
||||
return forcedProtocol === "sixel" && $flag("PI_ALLOW_SIXEL_PASSTHROUGH");
|
||||
}
|
||||
/** Returns true when the text contains a SIXEL start sequence. */
|
||||
export function containsSixelSequence(text: string): boolean {
|
||||
|
||||
@@ -415,9 +415,13 @@ type Server struct {
|
||||
|
||||
test("replace with empty content on last impl method collapses extra whitespace-only lines before the closing brace", () => {
|
||||
const source = `impl S {
|
||||
fn a() {}
|
||||
fn a() {
|
||||
keep();
|
||||
}
|
||||
|
||||
fn b() {}
|
||||
fn b() {
|
||||
drop();
|
||||
}
|
||||
|
||||
}
|
||||
`;
|
||||
@@ -429,7 +433,7 @@ type Server struct {
|
||||
operations: [{ op: "replace", sel: targetWithChecksum("impl_S.fn_b", crc), content: "" }],
|
||||
});
|
||||
|
||||
expect(result.diffSourceAfter).toBe("impl S {\n fn a() {}\n\n}\n");
|
||||
expect(result.diffSourceAfter).toBe("impl S {\n fn a() {\n keep();\n }\n\n}\n");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -57,13 +57,16 @@ ${ret}
|
||||
describe("chunk mode tools", () => {
|
||||
let tmpDir: string;
|
||||
let originalEditVariant: string | undefined;
|
||||
let originalChunkAutoIndent: string | undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "chunk-mode-test-"));
|
||||
_resetSettingsForTest();
|
||||
await Settings.init({ inMemory: true, cwd: tmpDir });
|
||||
originalEditVariant = Bun.env.PI_EDIT_VARIANT;
|
||||
originalChunkAutoIndent = Bun.env.PI_CHUNK_AUTOINDENT;
|
||||
Bun.env.PI_EDIT_VARIANT = "chunk";
|
||||
delete Bun.env.PI_CHUNK_AUTOINDENT;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -74,6 +77,11 @@ describe("chunk mode tools", () => {
|
||||
} else {
|
||||
Bun.env.PI_EDIT_VARIANT = originalEditVariant;
|
||||
}
|
||||
if (originalChunkAutoIndent === undefined) {
|
||||
delete Bun.env.PI_CHUNK_AUTOINDENT;
|
||||
} else {
|
||||
Bun.env.PI_CHUNK_AUTOINDENT = originalChunkAutoIndent;
|
||||
}
|
||||
});
|
||||
|
||||
it("reads a TypeScript file as a chunk listing in chunk mode", async () => {
|
||||
@@ -90,6 +98,31 @@ describe("chunk mode tools", () => {
|
||||
expect(text).not.toContain("ck:");
|
||||
});
|
||||
|
||||
it("documents default chunk auto-indent behavior without exposing a tool parameter", () => {
|
||||
const session = createSession(tmpDir);
|
||||
const readTool = new ReadTool(session);
|
||||
const editTool = new EditTool(session);
|
||||
|
||||
expect(readTool.description).toContain("normalize leading indentation");
|
||||
expect(readTool.description).not.toContain("indent_mode");
|
||||
expect(JSON.stringify(readTool.parameters)).not.toContain("indent_mode");
|
||||
expect(editTool.description).toContain("Use `\\t` for indentation");
|
||||
expect(editTool.description).not.toContain("indent_mode");
|
||||
expect(JSON.stringify(editTool.parameters)).not.toContain("indent_mode");
|
||||
});
|
||||
|
||||
it("adjusts chunk prompts when PI_CHUNK_AUTOINDENT=0", () => {
|
||||
Bun.env.PI_CHUNK_AUTOINDENT = "0";
|
||||
const session = createSession(tmpDir);
|
||||
const readTool = new ReadTool(session);
|
||||
const editTool = new EditTool(session);
|
||||
|
||||
expect(readTool.description).toContain("preserve literal leading tabs/spaces");
|
||||
expect(readTool.description).not.toContain("indent_mode");
|
||||
expect(editTool.description).toContain("Match the file's literal tabs/spaces");
|
||||
expect(editTool.description).not.toContain("indent_mode");
|
||||
});
|
||||
|
||||
it("disables hashline display mode across shared tool renderers in chunk mode", () => {
|
||||
const displayMode = resolveFileDisplayMode(createSession(tmpDir));
|
||||
|
||||
@@ -112,6 +145,24 @@ describe("chunk mode tools", () => {
|
||||
expect(text).toContain("return err.message + total;");
|
||||
});
|
||||
|
||||
it("preserves literal spaces in chunk reads when PI_CHUNK_AUTOINDENT=0", async () => {
|
||||
Bun.env.PI_CHUNK_AUTOINDENT = "0";
|
||||
const filePath = path.join(tmpDir, "server.ts");
|
||||
await Bun.write(filePath, buildLargeTypescriptFixture());
|
||||
const tool = new ReadTool(createSession(tmpDir));
|
||||
|
||||
const result = await tool.execute("chunk-read-preserve-indent", {
|
||||
path: `${filePath}:class_Server.fn_handleError`,
|
||||
});
|
||||
const text = getText(result);
|
||||
|
||||
expect(text).toContain("2| private handleError(err: Error): string {");
|
||||
expect(text).toContain("3| let total = 0;");
|
||||
expect(text).toContain("29| total +=");
|
||||
expect(text).not.toContain("2| \tprivate handleError");
|
||||
expect(text).not.toContain("29| \t\t\ttotal +=");
|
||||
});
|
||||
|
||||
it("renders line-range reads as range-scoped chunk output", async () => {
|
||||
const filePath = path.join(tmpDir, "server.ts");
|
||||
await Bun.write(filePath, buildLargeTypescriptFixture());
|
||||
@@ -236,6 +287,34 @@ describe("chunk mode tools", () => {
|
||||
expect(editText).toContain("@@ -3,62 +3,1 @@");
|
||||
});
|
||||
|
||||
it("preserves literal tabs in chunk edits when PI_CHUNK_AUTOINDENT=0", async () => {
|
||||
Bun.env.PI_CHUNK_AUTOINDENT = "0";
|
||||
const filePath = path.join(tmpDir, "preserve-tabs.ts");
|
||||
const source = 'class Server {\n handle(): void {\n console.log("old");\n }\n}\n';
|
||||
await Bun.write(filePath, source);
|
||||
const session = createSession(tmpDir);
|
||||
const editTool = new EditTool(session);
|
||||
const checksum = getChunkChecksum(source, "typescript", "class_Server.fn_handle");
|
||||
|
||||
const result = await editTool.execute("chunk-edit-preserve-indent", {
|
||||
path: filePath,
|
||||
edits: [
|
||||
{
|
||||
sel: `class_Server.fn_handle#${checksum}@body`,
|
||||
op: "replace",
|
||||
content: 'if (flag) {\n\tconsole.log("tabbed");\n}\n',
|
||||
},
|
||||
],
|
||||
} as never);
|
||||
const text = getText(result);
|
||||
const updatedSource = await Bun.file(filePath).text();
|
||||
|
||||
expect(updatedSource).toContain(
|
||||
' handle(): void {\n if (flag) {\n \tconsole.log("tabbed");\n }\n }\n',
|
||||
);
|
||||
expect(text).toContain('\tconsole.log("tabbed");');
|
||||
});
|
||||
|
||||
it("applies omitted-sel batch splices when the second operation uses the post-first checksum", async () => {
|
||||
const filePath = path.join(tmpDir, "server.ts");
|
||||
const originalSource = buildLargeTypescriptFixture();
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added `normalizeIndent` option to `EditParams` to control indentation normalization for response rendering and inserted content
|
||||
- Added `hasConflicts()` method to detect unresolved merge conflicts in parsed files
|
||||
- Added `conflictCount()` method to count unresolved merge conflicts in the chunk tree
|
||||
|
||||
|
||||
Vendored
+5
@@ -522,6 +522,11 @@ export interface EditOperation {
|
||||
export interface EditParams {
|
||||
/** Edits to apply in order. */
|
||||
operations: Array<EditOperation>
|
||||
/**
|
||||
* When true, normalize indentation for response rendering and inserted
|
||||
* content. When false, preserve literal tabs/spaces.
|
||||
*/
|
||||
normalizeIndent?: boolean
|
||||
/** Default chunk selector when an `EditOperation` omits `sel`. */
|
||||
defaultSelector?: string
|
||||
/** Default checksum when an `EditOperation` omits `crc`. */
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { getCrashLogPath, getDebugLogPath } from "@oh-my-pi/pi-utils";
|
||||
import { $flag, getCrashLogPath, getDebugLogPath } from "@oh-my-pi/pi-utils";
|
||||
import { isKeyRelease, matchesKey } from "./keys";
|
||||
import type { Terminal } from "./terminal";
|
||||
import { ImageProtocol, setCellDimensions, setTerminalImageProtocol, TERMINAL } from "./terminal-capabilities";
|
||||
@@ -228,8 +228,8 @@ export class TUI extends Container {
|
||||
#sixelProbeBuffer = "";
|
||||
#sixelProbeTimeout?: NodeJS.Timeout;
|
||||
#sixelProbeUnsubscribe?: () => void;
|
||||
#showHardwareCursor = process.env.PI_HARDWARE_CURSOR === "1";
|
||||
#clearOnShrink = process.env.PI_CLEAR_ON_SHRINK === "1"; // Clear empty rows when content shrinks (default: off)
|
||||
#showHardwareCursor = $flag("PI_HARDWARE_CURSOR");
|
||||
#clearOnShrink = $flag("PI_CLEAR_ON_SHRINK"); // Clear empty rows when content shrinks (default: off)
|
||||
#maxLinesRendered = 0; // High-water line count used for clear-on-shrink policy
|
||||
#fullRedrawCount = 0;
|
||||
#stopped = false;
|
||||
@@ -1035,7 +1035,7 @@ export class TUI extends Container {
|
||||
this.#previousHeight = height;
|
||||
};
|
||||
|
||||
const debugRedraw = process.env.PI_DEBUG_REDRAW === "1";
|
||||
const debugRedraw = $flag("PI_DEBUG_REDRAW");
|
||||
const logRedraw = (reason: string): void => {
|
||||
if (!debugRedraw) return;
|
||||
const logPath = getDebugLogPath();
|
||||
@@ -1246,7 +1246,7 @@ export class TUI extends Container {
|
||||
|
||||
buffer += "\x1b[?2026l"; // End synchronized output
|
||||
|
||||
if (process.env.PI_TUI_DEBUG === "1") {
|
||||
if ($flag("PI_TUI_DEBUG")) {
|
||||
const debugDir = "/tmp/tui";
|
||||
fs.mkdirSync(debugDir, { recursive: true });
|
||||
const debugPath = path.join(debugDir, `render-${Date.now()}-${Math.random().toString(36).slice(2)}.log`);
|
||||
|
||||
@@ -82,3 +82,27 @@ export function $pickenv(...keys: string[]): string | undefined {
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a positive decimal integer from `$env[name]`.
|
||||
* Empty, invalid, NaN, zero, or negative values return `defaultValue`.
|
||||
*/
|
||||
export function $envpos(name: string, defaultValue: number): number {
|
||||
const raw = $env[name];
|
||||
if (!raw) return defaultValue;
|
||||
const parsed = Number.parseInt(raw, 10);
|
||||
if (Number.isNaN(parsed) || parsed <= 0) return defaultValue;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/** True when `BUN_ENV` or `NODE_ENV` is the string `test`. */
|
||||
export function isBunTestRuntime(): boolean {
|
||||
return Bun.env.BUN_ENV === "test" || Bun.env.NODE_ENV === "test";
|
||||
}
|
||||
|
||||
const TRUTHY: Dict<boolean> = { "1": true, TRUE: true, YES: true, ON: true };
|
||||
export function $flag(name: string, def: boolean = false): boolean {
|
||||
const value = $env[name];
|
||||
if (!value) return def;
|
||||
return !!TRUTHY[value];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user