refactor(chunk): migrated chunk naming to type-safe ChunkKind enum

- Replaced string-based chunk naming with type-safe ChunkKind enum across 35+ AST classifier modules.
- Refactored ChunkNode to use optional identifier and ChunkKind instead of name string for semantic clarity.
- Created new kind.rs module with ChunkKind enum (150+ variants) and trait-based metadata system.
- Removed string prefix-based chunk type detection and replaced with ChunkKind enum matching throughout.
- Updated chunk API signatures to accept ChunkKind and optional identifier instead of formatted names.
This commit is contained in:
can1357
2026-04-08 10:17:51 +02:00
parent 7a4ef2b8d6
commit 79cbd1ed47
47 changed files with 2564 additions and 1518 deletions
+65 -38
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct AstroClassifier;
@@ -27,89 +27,116 @@ impl LangClassifier for AstroClassifier {
fn classify_astro_node<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
match node.kind() {
"frontmatter" => Some(classify_frontmatter(node, source)),
"frontmatter_js_block" => Some(group_candidate(node, "code", source)),
"frontmatter_js_block" => Some(group_candidate(node, ChunkKind::Code, source)),
"element" => classify_element(node, source),
"script_element" => Some(classify_script_element(node, source)),
"style_element" => Some(classify_style_element(node, source)),
"html_interpolation" => Some(classify_html_interpolation(node, source)),
"attribute_interpolation" => Some(classify_attribute_interpolation(node, source)),
"attribute_js_expr" => Some(group_candidate(node, "expr", source)),
"text" => Some(group_candidate(node, "text", source)),
"attribute_js_expr" => Some(group_candidate(node, ChunkKind::Expression, source)),
"text" => Some(group_candidate(node, ChunkKind::Text, source)),
_ => None,
}
}
fn classify_frontmatter<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
make_container_chunk(
force_container(make_container_chunk(
node,
"frontmatter".to_string(),
ChunkKind::Frontmatter,
None,
source,
recurse_into(node, ChunkContext::ClassBody, &[], &["frontmatter_js_block"]),
)
))
}
fn classify_element<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
let tag_name = extract_tag_name(node, source)?;
let prefix = if is_component_name(tag_name.as_str()) {
"component"
let recurse = Some(recurse_self(node, ChunkContext::ClassBody));
if is_component_name(tag_name.as_str()) {
Some(force_container(make_explicit_candidate(
node,
ChunkKind::Tag,
format!("component_{tag_name}"),
source,
recurse,
)))
} else {
"tag"
};
Some(make_container_chunk(
node,
format!("{prefix}_{tag_name}"),
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
))
Some(force_container(make_container_chunk(
node,
ChunkKind::Tag,
Some(tag_name),
source,
recurse,
)))
}
}
fn classify_script_element<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name = if has_attribute(node, "is:inline", source) {
"script_inline"
} else {
"script"
};
let identifier = has_attribute(node, "is:inline", source).then_some("inline".to_string());
// The Astro grammar exposes script bodies as `raw_text`, not nested JS AST.
make_named_chunk(node, name.to_string(), source, None)
make_kind_chunk(node, ChunkKind::Script, identifier, source, None)
}
fn classify_style_element<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name = if has_attribute(node, "define:vars", source) {
"style_vars"
let identifier = if has_attribute(node, "define:vars", source) {
Some("vars".to_string())
} else if has_attribute(node, "is:global", source) {
"style_global"
Some("global".to_string())
} else {
"style"
None
};
// The Astro grammar exposes style bodies as `raw_text`, so the section itself
// is the truthful chunk boundary.
make_named_chunk(node, name.to_string(), source, None)
make_kind_chunk(node, ChunkKind::Style, identifier, source, None)
}
fn classify_html_interpolation<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name = child_by_kind(node, &["permissible_text"])
.and_then(|expr| sanitize_identifier(node_text(source, expr.start_byte(), expr.end_byte())))
.map_or_else(|| "expr".to_string(), |expr| format!("expr_{expr}"));
let identifier = child_by_kind(node, &["permissible_text"])
.and_then(|expr| sanitize_identifier(node_text(source, expr.start_byte(), expr.end_byte())));
if let Some(nested_element) =
child_by_kind(node, &["element", "script_element", "style_element"])
{
make_container_chunk(
force_container(make_container_chunk(
node,
name,
ChunkKind::Expression,
identifier,
source,
Some(recurse_self(nested_element, ChunkContext::ClassBody)),
)
))
} else {
make_named_chunk(node, name, source, None)
make_kind_chunk(node, ChunkKind::Expression, identifier, source, None)
}
}
fn classify_attribute_interpolation<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name = child_by_kind(node, &["attribute_js_expr"])
let identifier = child_by_kind(node, &["attribute_js_expr"])
.and_then(|expr| sanitize_identifier(node_text(source, expr.start_byte(), expr.end_byte())))
.map_or_else(|| "attr_expr".to_string(), |expr| format!("attr_expr_{expr}"));
make_named_chunk(node, name, source, None)
.map_or_else(|| "expr".to_string(), |expr| format!("expr_{expr}"));
make_kind_chunk(node, ChunkKind::Attr, Some(identifier), source, None)
}
fn make_explicit_candidate<'t>(
node: Node<'t>,
kind: ChunkKind,
identifier: impl Into<Option<String>>,
source: &str,
recurse: Option<RecurseSpec<'t>>,
) -> RawChunkCandidate<'t> {
make_candidate(
node,
kind,
identifier,
NameStyle::Named,
signature_for_node(node, source),
recurse,
source,
)
}
const fn force_container(mut candidate: RawChunkCandidate<'_>) -> RawChunkCandidate<'_> {
candidate.force_recurse = true;
candidate
}
fn extract_tag_name(node: Node<'_>, source: &str) -> Option<String> {
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct ShellBuildClassifier;
@@ -29,7 +29,8 @@ impl LangClassifier for ShellBuildClassifier {
Self::extract_rule_target(node, source).unwrap_or_else(|| "anonymous".to_string());
Some(make_container_chunk(
node,
format!("rule_{name}"),
ChunkKind::Rule,
Some(name),
source,
recurse_into(node, ChunkContext::ClassBody, &[], &["recipe"]),
))
@@ -37,30 +38,32 @@ impl LangClassifier for ShellBuildClassifier {
"variable_assignment" | "shell_assignment" => {
let name =
Self::extract_var_name(node, source).unwrap_or_else(|| "anonymous".to_string());
Some(make_named_chunk(node, format!("var_{name}"), source, None))
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_named_chunk(node, format!("define_{name}"), source, None))
Some(make_kind_chunk(node, ChunkKind::Define, Some(name), source, None))
},
"conditional" => Some(positional_candidate(node, "if", source)),
"conditional" => Some(positional_candidate(node, ChunkKind::If, source)),
// Bash commands and pipelines
"command" | "pipeline" => Some(group_candidate(node, "stmts", source)),
"command" | "pipeline" => Some(group_candidate(node, ChunkKind::Statements, source)),
// Bash control flow
"if_statement" => Some(positional_candidate(node, "if", source)),
"case_statement" => Some(positional_candidate(node, "switch", source)),
"while_statement" | "for_statement" => Some(positional_candidate(node, "loop", source)),
"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,
"fn",
ChunkKind::Function,
source,
recurse_body(node, ChunkContext::FunctionBody),
)),
// Diff nodes
"hunks" => Some(group_candidate(node, "hunks", source)),
"file_change" => Some(named_candidate(node, "file", source, None)),
"hunks" => Some(group_candidate(node, ChunkKind::Hunks, source)),
"file_change" => Some(named_candidate(node, ChunkKind::File, source, None)),
_ => None,
}
}
@@ -71,11 +74,13 @@ impl LangClassifier for ShellBuildClassifier {
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
match node.kind() {
"if_statement" => Some(positional_candidate(node, "if", source)),
"case_statement" => Some(positional_candidate(node, "switch", source)),
"while_statement" | "for_statement" => Some(positional_candidate(node, "loop", source)),
"command" | "pipeline" => Some(group_candidate(node, "stmts", source)),
"subshell" => Some(positional_candidate(node, "block", source)),
"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)),
_ => None,
}
}
+71 -94
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*, defaults::classify_var_decl};
use super::{classify::LangClassifier, common::*, defaults::classify_var_decl, kind::ChunkKind};
pub struct CCppClassifier;
@@ -94,30 +94,27 @@ fn extract_c_field_name(node: Node<'_>, source: &str) -> Option<String> {
extract_c_declarator_name(decl, source)
}
/// Build a prefixed name for a C/C++ function node using declarator traversal.
fn c_prefixed_fn_name(prefix: &str, node: Node<'_>, source: &str) -> String {
let identifier =
extract_c_function_name(node, source).unwrap_or_else(|| "anonymous".to_string());
format!("{prefix}_{identifier}")
}
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, "imports", source)),
| "import_declaration" | "module_import" => {
Some(group_candidate(node, ChunkKind::Imports, source))
},
// ── Functions ──
"function_definition" | "function_declaration" => Some(make_named_chunk(
"function_definition" | "function_declaration" => Some(make_kind_chunk(
node,
c_prefixed_fn_name("fn", node, source),
ChunkKind::Function,
extract_c_function_name(node, source),
source,
recurse_body(node, ChunkContext::FunctionBody),
)),
"constructor_definition" => Some(make_named_chunk(
"constructor_definition" => Some(make_kind_chunk(
node,
"constructor".to_string(),
ChunkKind::Constructor,
None,
source,
recurse_body(node, ChunkContext::FunctionBody),
)),
@@ -144,47 +141,50 @@ impl LangClassifier for CCppClassifier {
candidate.checksum_start_byte = node.start_byte();
Some(candidate)
},
None => Some(named_candidate(
None => Some(make_candidate(
node,
"template",
source,
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, "class", source, recurse_class(node)))
Some(container_candidate(node, ChunkKind::Class, source, recurse_class(node)))
},
"struct_specifier" | "struct_declaration" => {
Some(container_candidate(node, "struct", source, recurse_class(node)))
Some(container_candidate(node, ChunkKind::Struct, source, recurse_class(node)))
},
"enum_specifier" | "enum_declaration" => {
Some(container_candidate(node, "enum", source, recurse_enum(node)))
Some(container_candidate(node, ChunkKind::Enum, source, recurse_enum(node)))
},
"namespace_definition" => {
Some(container_candidate(node, "mod", source, recurse_class(node)))
Some(container_candidate(node, ChunkKind::Module, source, recurse_class(node)))
},
"union_declaration" => {
Some(container_candidate(node, "union", source, recurse_class(node)))
Some(container_candidate(node, ChunkKind::Union, source, recurse_class(node)))
},
// ── Types ──
"type_alias_declaration" | "user_defined_type_definition" => {
Some(named_candidate(node, "type", source, recurse_class(node)))
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, "decls", source))
Some(group_candidate(node, ChunkKind::Declarations, source))
},
// ── Macros ──
"macro_definition" => Some(named_candidate(
node,
"macro",
ChunkKind::Macro,
source,
recurse_body(node, ChunkContext::FunctionBody),
)),
@@ -194,7 +194,7 @@ impl LangClassifier for CCppClassifier {
| "do_statement" | "try_block" => Some(classify_function_c(node, source)),
// ── Statements ──
"expression_statement" => Some(group_candidate(node, "stmts", source)),
"expression_statement" => Some(group_candidate(node, ChunkKind::Statements, source)),
_ => None,
}
@@ -208,16 +208,18 @@ impl LangClassifier for CCppClassifier {
.or_else(|| extract_identifier(node, source))
.unwrap_or_else(|| "anonymous".to_string());
if name == "constructor" {
Some(make_named_chunk(
Some(make_kind_chunk(
node,
"constructor".to_string(),
ChunkKind::Constructor,
None,
source,
recurse_body(node, ChunkContext::FunctionBody),
))
} else {
Some(make_named_chunk(
Some(make_kind_chunk(
node,
format!("fn_{name}"),
ChunkKind::Function,
Some(name),
source,
recurse_body(node, ChunkContext::FunctionBody),
))
@@ -225,40 +227,41 @@ impl LangClassifier for CCppClassifier {
},
// ── Constructors ──
"constructor_definition" | "constructor_declaration" => Some(make_named_chunk(
"constructor_definition" | "constructor_declaration" => Some(make_kind_chunk(
node,
"constructor".to_string(),
ChunkKind::Constructor,
None,
source,
recurse_body(node, ChunkContext::FunctionBody),
)),
// ── Fields ──
"field_declaration" => Some(match extract_c_field_name(node, source) {
Some(name) => make_named_chunk(node, format!("field_{name}"), source, None),
None => group_candidate(node, "fields", 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_named_chunk(node, format!("variant_{name}"), source, None),
None => group_candidate(node, "variants", 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, "class", source, recurse_class(node)))
Some(container_candidate(node, ChunkKind::Class, source, recurse_class(node)))
},
"struct_specifier" | "struct_declaration" => {
Some(container_candidate(node, "struct", source, recurse_class(node)))
Some(container_candidate(node, ChunkKind::Struct, source, recurse_class(node)))
},
"enum_specifier" | "enum_declaration" => {
Some(container_candidate(node, "enum", source, recurse_enum(node)))
Some(container_candidate(node, ChunkKind::Enum, source, recurse_enum(node)))
},
"union_declaration" => {
Some(container_candidate(node, "union", source, recurse_class(node)))
Some(container_candidate(node, ChunkKind::Union, source, recurse_class(node)))
},
"namespace_definition" => {
Some(container_candidate(node, "mod", source, recurse_class(node)))
Some(container_candidate(node, ChunkKind::Module, source, recurse_class(node)))
},
// ── Templates (class body) ──
@@ -281,17 +284,20 @@ impl LangClassifier for CCppClassifier {
candidate.checksum_start_byte = node.start_byte();
Some(candidate)
},
None => Some(named_candidate(
None => Some(make_candidate(
node,
"template",
source,
ChunkKind::Template,
None,
NameStyle::Named,
signature_for_node(node, source),
recurse_body(node, ChunkContext::FunctionBody),
source,
)),
}
},
// ── Types ──
"type_alias_declaration" => Some(named_candidate(node, "type", source, None)),
"type_alias_declaration" => Some(named_candidate(node, ChunkKind::Type, source, None)),
_ => None,
}
@@ -306,68 +312,39 @@ fn classify_function_c<'tree>(node: Node<'tree>, source: &str) -> RawChunkCandid
let fn_recurse = || recurse_body(node, ChunkContext::FunctionBody);
match node.kind() {
"if_statement" => {
make_candidate(node, "if".to_string(), NameStyle::Named, None, fn_recurse(), false, source)
make_candidate(node, ChunkKind::If, None, NameStyle::Named, None, fn_recurse(), source)
},
"switch_statement" => {
make_candidate(node, ChunkKind::Switch, None, NameStyle::Named, None, fn_recurse(), source)
},
"try_block" | "catch_clause" | "finally_clause" => {
make_candidate(node, ChunkKind::Try, None, NameStyle::Named, None, fn_recurse(), source)
},
"for_statement" => {
make_candidate(node, ChunkKind::For, None, NameStyle::Named, None, fn_recurse(), source)
},
"while_statement" => {
make_candidate(node, ChunkKind::While, None, NameStyle::Named, None, fn_recurse(), source)
},
"do_statement" => {
make_candidate(node, ChunkKind::Block, None, NameStyle::Named, None, fn_recurse(), source)
},
"switch_statement" => make_candidate(
node,
"switch".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"try_block" | "catch_clause" | "finally_clause" => make_candidate(
node,
"try".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"for_statement" => make_candidate(
node,
"for".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"while_statement" => make_candidate(
node,
"while".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"do_statement" => make_candidate(
node,
"block".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"variable_declaration" => {
let span = line_span(node.start_position().row + 1, node.end_position().row + 1);
if span > 1 {
if let Some(name) = extract_single_declarator_name(node, source) {
make_named_chunk(node, format!("var_{name}"), source, None)
make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None)
} else {
group_candidate(node, "variable", source)
group_candidate(node, ChunkKind::Variable, source)
}
} else {
group_candidate(node, "variable", source)
group_candidate(node, ChunkKind::Variable, source)
}
},
_ => {
let kind_name = sanitize_node_kind(node.kind());
group_candidate(node, &kind_name, source)
let kind = ChunkKind::from_sanitized_kind(kind_name.as_str());
group_candidate(node, kind, source)
},
}
}
+9 -15
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct ClojureClassifier;
@@ -25,35 +25,29 @@ fn form_name(node: Node<'_>, source: &str) -> Option<String> {
})
}
/// Build a prefixed name from a Clojure form, falling back to "anonymous".
fn form_prefixed_name(node: Node<'_>, prefix: &str, source: &str) -> String {
let name = form_name(node, source).unwrap_or_else(|| "anonymous".to_string());
format!("{prefix}_{name}")
}
/// Classify a `list_lit` Clojure form based on its head symbol.
fn classify_form<'t>(node: Node<'t>, source: &str, at_root: bool) -> RawChunkCandidate<'t> {
let Some(head) = form_head(node, source) else {
return positional_candidate(node, "form", source);
return positional_candidate(node, ChunkKind::Form, source);
};
match head.as_str() {
"ns" | "require" | "use" | "import" | "refer-clojure" => {
group_candidate(node, "imports", source)
group_candidate(node, ChunkKind::Imports, source)
},
"defn" | "defn-" | "defmacro" | "defmulti" | "defmethod" => {
make_named_chunk(node, form_prefixed_name(node, "fn", source), source, None)
make_kind_chunk(node, ChunkKind::Function, form_name(node, source), source, None)
},
"def" | "defonce" => {
make_named_chunk(node, form_prefixed_name(node, "decl", source), source, None)
make_kind_chunk(node, ChunkKind::Decl, form_name(node, source), source, None)
},
"defprotocol" => {
make_container_chunk(node, form_prefixed_name(node, "proto", source), source, None)
make_container_chunk(node, ChunkKind::Proto, form_name(node, source), source, None)
},
"deftype" | "defrecord" | "extend-type" | "extend-protocol" => {
make_container_chunk(node, form_prefixed_name(node, "type", source), source, None)
make_container_chunk(node, ChunkKind::Type, form_name(node, source), source, None)
},
_ if at_root => positional_candidate(node, "form", source),
_ => group_candidate(node, "block", source),
_ if at_root => positional_candidate(node, ChunkKind::Form, source),
_ => group_candidate(node, ChunkKind::Block, source),
}
}
+22 -17
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct CMakeClassifier;
@@ -47,7 +47,8 @@ fn classify_definition<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandi
let name = nth_argument_name(header, 0, source).unwrap_or_else(|| "anonymous".to_string());
Some(make_container_chunk(
node,
format!("fn_{name}"),
ChunkKind::Function,
Some(name),
source,
recurse_into(node, ChunkContext::FunctionBody, &[], &["body"]),
))
@@ -57,20 +58,23 @@ fn classify_definition<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandi
let name = nth_argument_name(header, 0, source).unwrap_or_else(|| "anonymous".to_string());
Some(make_container_chunk(
node,
format!("macro_{name}"),
ChunkKind::Macro,
Some(name),
source,
recurse_into(node, ChunkContext::FunctionBody, &[], &["body"]),
))
},
"if_condition" => Some(make_container_chunk(
node,
"if".to_string(),
ChunkKind::If,
None,
source,
Some(recurse_self(node, ChunkContext::FunctionBody)),
)),
"foreach_loop" | "while_loop" => Some(make_container_chunk(
node,
"loop".to_string(),
ChunkKind::Loop,
None,
source,
recurse_into(node, ChunkContext::FunctionBody, &[], &["body"]),
)),
@@ -85,37 +89,38 @@ fn classify_command<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidat
let command = command_name(node, source)?;
Some(match command.as_str() {
"cmake_minimum_required" => make_named_chunk(node, "version_gate".to_string(), source, None),
"cmake_minimum_required" => make_kind_chunk(node, ChunkKind::VersionGate, None, source, None),
"project" => {
let name = nth_argument_name(node, 0, source).unwrap_or_else(|| "anonymous".to_string());
make_named_chunk(node, format!("project_{name}"), source, None)
make_kind_chunk(node, ChunkKind::Project, Some(name), source, None)
},
"include" | "find_package" => group_candidate(node, "imports", source),
"include" | "find_package" => group_candidate(node, ChunkKind::Imports, source),
"option" => {
let name = nth_argument_name(node, 0, source).unwrap_or_else(|| "anonymous".to_string());
make_named_chunk(node, format!("option_{name}"), source, None)
make_kind_chunk(node, ChunkKind::Option, Some(name), source, None)
},
"set" => {
let name = nth_argument_name(node, 0, source).unwrap_or_else(|| "anonymous".to_string());
make_named_chunk(node, format!("var_{name}"), source, None)
make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None)
},
"add_library" | "add_executable" | "add_custom_target" => {
let name = nth_argument_name(node, 0, source).unwrap_or_else(|| "anonymous".to_string());
make_named_chunk(node, format!("target_{name}"), source, None)
make_kind_chunk(node, ChunkKind::Target, Some(name), source, None)
},
"install" | "export" => group_candidate(node, "install", source),
other => group_candidate(node, &format!("cmd_{other}"), source),
"install" | "export" => group_candidate(node, ChunkKind::Install, source),
other => make_kind_chunk(node, ChunkKind::Cmd, Some(other.to_string()), source, None),
})
}
fn classify_if_child<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
match node.kind() {
"if_command" => Some(group_candidate(node, "cond", source)),
"elseif_command" => Some(positional_candidate(node, "elif", source)),
"else_command" => Some(positional_candidate(node, "else", source)),
"if_command" => Some(group_candidate(node, ChunkKind::Cond, source)),
"elseif_command" => Some(positional_candidate(node, ChunkKind::Elif, source)),
"else_command" => Some(positional_candidate(node, ChunkKind::Else, source)),
"body" => Some(make_container_chunk(
node,
"block".to_string(),
ChunkKind::Block,
None,
source,
Some(recurse_self(node, ChunkContext::FunctionBody)),
)),
+67 -94
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*, defaults::classify_var_decl};
use super::{classify::LangClassifier, common::*, defaults::classify_var_decl, kind::ChunkKind};
pub struct CSharpJavaClassifier;
@@ -13,54 +13,57 @@ impl LangClassifier for CSharpJavaClassifier {
"import_declaration"
| "using_directive"
| "package_declaration"
| "namespace_statement" => Some(group_candidate(node, "imports", source)),
| "namespace_statement" => Some(group_candidate(node, ChunkKind::Imports, source)),
// ── Functions ──
"method_declaration" => Some(named_candidate(
node,
"meth",
ChunkKind::Method,
source,
recurse_body(node, ChunkContext::FunctionBody),
)),
"function_declaration" | "function_definition" => Some(named_candidate(
node,
"fn",
ChunkKind::Function,
source,
recurse_body(node, ChunkContext::FunctionBody),
)),
// ── Constructors ──
"constructor_declaration" => Some(make_named_chunk(
"constructor_declaration" => Some(make_kind_chunk(
node,
"constructor".to_string(),
ChunkKind::Constructor,
None,
source,
recurse_body(node, ChunkContext::FunctionBody),
)),
// ── Containers ──
"class_declaration" => {
Some(container_candidate(node, "class", source, recurse_class(node)))
Some(container_candidate(node, ChunkKind::Class, source, recurse_class(node)))
},
"interface_declaration" => {
Some(container_candidate(node, "iface", source, recurse_interface(node)))
Some(container_candidate(node, ChunkKind::Iface, source, recurse_interface(node)))
},
"enum_declaration" => {
Some(container_candidate(node, ChunkKind::Enum, source, recurse_enum(node)))
},
"enum_declaration" => Some(container_candidate(node, "enum", source, recurse_enum(node))),
"namespace_declaration" | "file_scoped_namespace_declaration" => {
Some(container_candidate(node, "mod", source, recurse_class(node)))
Some(container_candidate(node, ChunkKind::Module, source, recurse_class(node)))
},
"struct_declaration" | "record_declaration" => {
Some(container_candidate(node, "struct", source, recurse_class(node)))
Some(container_candidate(node, ChunkKind::Struct, source, recurse_class(node)))
},
// ── Types ──
"type_alias_declaration" => {
Some(named_candidate(node, "type", source, recurse_class(node)))
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, "decls", source))
Some(group_candidate(node, ChunkKind::Declarations, source))
},
// ── Control flow (top-level scripts) ──
@@ -70,7 +73,7 @@ impl LangClassifier for CSharpJavaClassifier {
},
// ── Statements ──
"expression_statement" => Some(group_candidate(node, "stmts", source)),
"expression_statement" => Some(group_candidate(node, ChunkKind::Statements, source)),
_ => None,
}
@@ -80,33 +83,37 @@ impl LangClassifier for CSharpJavaClassifier {
match node.kind() {
// ── Container declarations (inside namespace/class bodies) ──
"class_declaration" => {
Some(container_candidate(node, "class", source, recurse_class(node)))
Some(container_candidate(node, ChunkKind::Class, source, recurse_class(node)))
},
"interface_declaration" => {
Some(container_candidate(node, "iface", source, recurse_interface(node)))
Some(container_candidate(node, ChunkKind::Iface, source, recurse_interface(node)))
},
"enum_declaration" => {
Some(container_candidate(node, ChunkKind::Enum, source, recurse_enum(node)))
},
"enum_declaration" => Some(container_candidate(node, "enum", source, recurse_enum(node))),
"struct_declaration" | "record_declaration" => {
Some(container_candidate(node, "struct", source, recurse_class(node)))
Some(container_candidate(node, ChunkKind::Struct, source, recurse_class(node)))
},
"namespace_declaration" | "file_scoped_namespace_declaration" => {
Some(container_candidate(node, "mod", source, recurse_class(node)))
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_named_chunk(
Some(make_kind_chunk(
node,
"constructor".to_string(),
ChunkKind::Constructor,
None,
source,
recurse_body(node, ChunkContext::FunctionBody),
))
} else {
Some(make_named_chunk(
Some(make_kind_chunk(
node,
format!("fn_{name}"),
ChunkKind::Function,
Some(name),
source,
recurse_body(node, ChunkContext::FunctionBody),
))
@@ -114,9 +121,10 @@ impl LangClassifier for CSharpJavaClassifier {
},
// ── Constructors ──
"constructor_declaration" | "secondary_constructor" => Some(make_named_chunk(
"constructor_declaration" | "secondary_constructor" => Some(make_kind_chunk(
node,
"constructor".to_string(),
ChunkKind::Constructor,
None,
source,
recurse_body(node, ChunkContext::FunctionBody),
)),
@@ -126,21 +134,21 @@ impl LangClassifier for CSharpJavaClassifier {
| "property_declaration"
| "constant_declaration"
| "event_field_declaration" => Some(match extract_field_name(node, source) {
Some(name) => make_named_chunk(node, format!("field_{name}"), source, None),
None => group_candidate(node, "fields", 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_named_chunk(node, format!("variant_{name}"), source, None),
None => group_candidate(node, "variants", 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_named_chunk(node, "static_init".to_string(), source, None))
Some(make_kind_chunk(node, ChunkKind::StaticInit, None, source, None))
},
_ => None,
@@ -176,79 +184,44 @@ fn classify_function_csharp_java<'tree>(
let fn_recurse = || recurse_body(node, ChunkContext::FunctionBody);
match node.kind() {
"if_statement" => {
make_candidate(node, "if".to_string(), NameStyle::Named, None, fn_recurse(), false, source)
make_candidate(node, ChunkKind::If, None, NameStyle::Named, None, fn_recurse(), source)
},
"switch_statement" | "switch_expression" => {
make_candidate(node, ChunkKind::Switch, None, NameStyle::Named, None, fn_recurse(), source)
},
"try_statement" | "catch_clause" | "finally_clause" => {
make_candidate(node, ChunkKind::Try, None, NameStyle::Named, None, fn_recurse(), source)
},
"for_statement" => {
make_candidate(node, ChunkKind::For, None, NameStyle::Named, None, fn_recurse(), source)
},
"foreach_statement" => {
make_candidate(node, ChunkKind::For, None, NameStyle::Named, None, fn_recurse(), source)
},
"while_statement" => {
make_candidate(node, ChunkKind::While, None, NameStyle::Named, None, fn_recurse(), source)
},
"do_statement" => {
make_candidate(node, ChunkKind::Block, None, NameStyle::Named, None, fn_recurse(), source)
},
"switch_statement" | "switch_expression" => make_candidate(
node,
"switch".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"try_statement" | "catch_clause" | "finally_clause" => make_candidate(
node,
"try".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"for_statement" => make_candidate(
node,
"for".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"foreach_statement" => make_candidate(
node,
"for".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"while_statement" => make_candidate(
node,
"while".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"do_statement" => make_candidate(
node,
"block".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"variable_declaration" | "lexical_declaration" => {
let span = line_span(node.start_position().row + 1, node.end_position().row + 1);
if span > 1 {
if let Some(name) = extract_single_declarator_name(node, source) {
make_named_chunk(node, format!("var_{name}"), source, None)
make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None)
} else {
let kind_name = sanitize_node_kind(node.kind());
group_candidate(node, &kind_name, source)
group_from_sanitized(node, source)
}
} else {
let kind_name = sanitize_node_kind(node.kind());
group_candidate(node, &kind_name, source)
group_from_sanitized(node, source)
}
},
_ => {
let kind_name = sanitize_node_kind(node.kind());
group_candidate(node, &kind_name, source)
},
_ => group_from_sanitized(node, source),
}
}
fn group_from_sanitized<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let kind_name = sanitize_node_kind(node.kind());
let kind = ChunkKind::from_sanitized_kind(kind_name.as_str());
make_candidate(node, kind, kind_name, NameStyle::Group, None, None, source)
}
+7 -9
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct CssClassifier;
@@ -29,7 +29,8 @@ fn classify_rule_set<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t>
let name = extract_css_selector(node, source).unwrap_or_else(|| "anonymous".to_string());
make_container_chunk(
node,
format!("rule_{name}"),
ChunkKind::Rule,
Some(name),
source,
recurse_into(node, ChunkContext::ClassBody, &[], &["block"]),
)
@@ -39,10 +40,10 @@ fn classify_rule_set<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t>
/// named container.
fn classify_at_rule<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name = extract_css_selector(node, source).unwrap_or_else(|| "rule".to_string());
// keyframes_statement uses `keyframe_block_list` as body; others use `block`
make_container_chunk(
node,
format!("at_{name}"),
ChunkKind::At,
Some(name),
source,
recurse_into(node, ChunkContext::ClassBody, &[], &["block", "keyframe_block_list"]),
)
@@ -53,19 +54,16 @@ fn classify_at_rule<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
fn classify_css_node<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
match node.kind() {
"rule_set" => Some(classify_rule_set(node, source)),
// tree-sitter-css emits specific `*_statement` kinds for known at-rules
// and generic `at_rule` for unknown ones (e.g. @font-face).
"at_rule" | "media_statement" | "keyframes_statement" | "supports_statement" => {
Some(classify_at_rule(node, source))
},
"keyframe_block" => Some(named_candidate(
node,
"frame",
ChunkKind::Frame,
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
)),
// Top-level or nested property declarations.
"declaration" => Some(group_candidate(node, "fields", source)),
"declaration" => Some(group_candidate(node, ChunkKind::Fields, source)),
_ => None,
}
}
+39 -15
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct DataFormatsClassifier;
@@ -33,65 +33,89 @@ fn classify_data_node<'t>(
// Key-value pairs (JSON pairs, YAML mappings)
"pair" => {
let name = extract_pair_key(node, source).unwrap_or_else(|| "anonymous".to_string());
Some(make_named_chunk(node, format!("key_{name}"), source, recurse_value_container(node)))
Some(make_kind_chunk(
node,
ChunkKind::Key,
Some(name),
source,
recurse_value_container(node),
))
},
"block_mapping_pair" | "flow_pair" => {
let name = extract_yaml_key(node, source).unwrap_or_else(|| "anonymous".to_string());
Some(make_named_chunk(node, format!("key_{name}"), source, recurse_value_container(node)))
Some(make_kind_chunk(
node,
ChunkKind::Key,
Some(name),
source,
recurse_value_container(node),
))
},
// TOML tables
"table" => Some(container_candidate(
node,
"table",
ChunkKind::Table,
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
)),
// TOML array tables [[...]]
"table_array_element" => Some(container_candidate(
// TOML array tables
"table_array_element" => Some(make_candidate(
node,
"table_array",
source,
ChunkKind::Table,
"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,
"table".to_string(),
ChunkKind::Table,
None,
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
)),
// JSON objects
"object" => Some(make_container_chunk(
node,
"object".to_string(),
ChunkKind::Object,
None,
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
)),
// JSON arrays
"array" => Some(make_container_chunk(
node,
"array".to_string(),
ChunkKind::Array,
None,
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
)),
// YAML block/flow mappings
"block_mapping" | "flow_mapping" => Some(make_container_chunk(
node,
"map".to_string(),
ChunkKind::Map,
None,
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
)),
// YAML block/flow sequences
"block_sequence" | "flow_sequence" => Some(make_container_chunk(
node,
"list".to_string(),
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, "item", source)),
"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, "attr", source, recurse_value_container(node))),
"attribute" => {
Some(named_candidate(node, ChunkKind::Attr, source, recurse_value_container(node)))
},
_ => None,
}
}
+23 -14
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct DockerfileClassifier;
@@ -53,7 +53,15 @@ fn classify_command_instruction<'t>(node: Node<'t>, source: &str) -> Option<RawC
_ => return None,
};
Some(make_container_chunk(node, chunk_name.to_string(), source, recurse_command(node)))
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 {
@@ -61,36 +69,37 @@ impl LangClassifier for DockerfileClassifier {
match node.kind() {
"from_instruction" => {
let name = extract_stage_name(node, source).unwrap_or_else(|| "anonymous".to_string());
Some(make_named_chunk(node, format!("stage_{name}"), source, None))
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_named_chunk(node, format!("arg_{name}"), source, None))
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_named_chunk(node, format!("env_{name}"), source, None))
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_named_chunk(node, format!("label_{name}"), source, None))
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,
"healthcheck".to_string(),
ChunkKind::Healthcheck,
None,
source,
recurse_into(node, ChunkContext::FunctionBody, &[], &["cmd_instruction"]),
)),
"copy_instruction" => Some(group_candidate(node, "copy", source)),
"add_instruction" => Some(group_candidate(node, "add", source)),
"workdir_instruction" => Some(group_candidate(node, "workdir", source)),
"expose_instruction" => Some(group_candidate(node, "expose", source)),
"user_instruction" => Some(group_candidate(node, "user", source)),
"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,
}
}
@@ -102,8 +111,8 @@ impl LangClassifier for DockerfileClassifier {
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, "shell", source)),
"json_string_array" => Some(group_candidate(node, "argv", source)),
"shell_command" => Some(group_candidate(node, ChunkKind::Shell, source)),
"json_string_array" => Some(group_candidate(node, ChunkKind::Argv, source)),
_ => None,
}
}
+21 -16
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct ElixirClassifier;
@@ -21,44 +21,49 @@ fn classify_call<'t>(node: Node<'t>, source: &str, at_root: bool) -> RawChunkCan
match target.as_str() {
"defmodule" => make_container_chunk(
node,
format!("mod_{}", name()),
ChunkKind::Module,
Some(name()),
source,
recurse_body(node, ChunkContext::ClassBody),
),
"defprotocol" => make_container_chunk(
node,
format!("proto_{}", name()),
ChunkKind::Proto,
Some(name()),
source,
recurse_body(node, ChunkContext::ClassBody),
),
"defimpl" => make_container_chunk(
node,
format!("impl_{}", name()),
ChunkKind::Impl,
Some(name()),
source,
recurse_body(node, ChunkContext::ClassBody),
),
"def" | "defp" | "defdelegate" | "defguard" | "defguardp" | "defn" | "defnp" => {
make_named_chunk(
make_kind_chunk(
node,
format!("fn_{}", name()),
ChunkKind::Function,
Some(name()),
source,
recurse_body(node, ChunkContext::FunctionBody),
)
},
"defmacro" | "defmacrop" => make_named_chunk(
"defmacro" | "defmacrop" => make_kind_chunk(
node,
format!("macro_{}", name()),
ChunkKind::Macro,
Some(name()),
source,
recurse_body(node, ChunkContext::FunctionBody),
),
"alias" | "import" | "require" | "use" => group_candidate(node, "imports", source),
"defstruct" | "defexception" => group_candidate(node, "decls", source),
"if" | "unless" => positional_candidate(node, "if", source),
"case" | "cond" | "receive" => positional_candidate(node, "switch", source),
"for" => positional_candidate(node, "for", source),
"try" | "with" => positional_candidate(node, "block", source),
_ if at_root => group_candidate(node, "stmts", source),
_ => group_candidate(node, "block", source),
"alias" | "import" | "require" | "use" => group_candidate(node, ChunkKind::Imports, source),
"defstruct" | "defexception" => group_candidate(node, ChunkKind::Declarations, source),
"if" | "unless" => positional_candidate(node, ChunkKind::If, source),
"case" | "cond" | "receive" => positional_candidate(node, ChunkKind::Switch, source),
"for" => positional_candidate(node, ChunkKind::For, source),
"try" | "with" => positional_candidate(node, ChunkKind::Block, source),
_ if at_root => group_candidate(node, ChunkKind::Statements, source),
_ => group_candidate(node, ChunkKind::Block, source),
}
}
+69 -34
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct ErlangClassifier;
@@ -54,28 +54,34 @@ impl LangClassifier for ErlangClassifier {
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
Some(match node.kind() {
"module_attribute" => {
make_named_chunk(node, format!("mod_{}", erlang_name(node, source)?), source, None)
make_kind_chunk(node, ChunkKind::Module, erlang_name(node, source), source, None)
},
"export_attribute" | "export_type_attribute" => group_candidate(node, "exports", source),
"import_attribute" => group_candidate(node, "imports", source),
"pp_include" | "pp_include_lib" => group_candidate(node, "includes", source),
"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_named_chunk(node, format!("macro_{}", erlang_name(node, source)?), source, None)
make_kind_chunk(node, ChunkKind::Macro, erlang_name(node, source), source, None)
},
"record_decl" => make_container_chunk(
"record_decl" => make_candidate(
node,
ChunkKind::Struct,
format!("record_{}", erlang_name(node, source)?),
source,
NameStyle::Named,
signature_for_node(node, source),
Some(recurse_self(node, ChunkContext::ClassBody)),
source,
),
"type_alias" => {
make_named_chunk(node, format!("type_{}", erlang_name(node, source)?), source, None)
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_named_chunk(
"fun_decl" => make_kind_chunk(
node,
format!("fn_{}", erlang_name(node, source)?),
ChunkKind::Function,
erlang_name(node, source),
source,
Some(recurse_self(node, ChunkContext::FunctionBody)),
),
@@ -87,7 +93,7 @@ impl LangClassifier for ErlangClassifier {
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
Some(match node.kind() {
"record_field" => {
make_named_chunk(node, format!("field_{}", erlang_name(node, source)?), source, None)
make_kind_chunk(node, ChunkKind::Field, erlang_name(node, source), source, None)
},
_ => return None,
})
@@ -95,42 +101,71 @@ impl LangClassifier for ErlangClassifier {
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
Some(match node.kind() {
"function_clause" => make_named_chunk(
"function_clause" => make_kind_chunk(
node,
format!("clause_{}", erlang_name(node, source)?),
ChunkKind::Clause,
erlang_name(node, source),
source,
recurse_clause_body(node),
),
"fun_clause" | "cr_clause" => {
make_named_chunk(node, "clause".to_string(), source, recurse_clause_body(node))
},
"receive_after" => {
make_named_chunk(node, "after".to_string(), source, recurse_clause_body(node))
},
"catch_clause" => {
make_named_chunk(node, "catch".to_string(), source, recurse_clause_body(node))
},
"receive_expr" => make_named_chunk(
"fun_clause" | "cr_clause" => make_candidate(
node,
"receive".to_string(),
ChunkKind::Clause,
None,
NameStyle::Named,
signature_for_node(node, source),
recurse_clause_body(node),
source,
Some(recurse_self(node, ChunkContext::FunctionBody)),
),
"case_expr" => make_named_chunk(
"receive_after" => make_candidate(
node,
"case".to_string(),
ChunkKind::After,
None,
NameStyle::Named,
signature_for_node(node, source),
recurse_clause_body(node),
source,
Some(recurse_self(node, ChunkContext::FunctionBody)),
),
"try_expr" => make_named_chunk(
"catch_clause" => make_candidate(
node,
"try".to_string(),
ChunkKind::Catch,
None,
NameStyle::Named,
signature_for_node(node, source),
recurse_clause_body(node),
source,
Some(recurse_self(node, ChunkContext::FunctionBody)),
),
"anonymous_fun" => make_named_chunk(
"receive_expr" => make_candidate(
node,
"fn_anonymous".to_string(),
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)),
),
+44 -49
View File
@@ -1,6 +1,6 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct GoClassifier;
@@ -8,26 +8,28 @@ 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, "imports", source)),
"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_named_chunk(node, format!("var_{name}"), source, None),
None => group_candidate(node, "decls", 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,
"fn",
ChunkKind::Function,
source,
recurse_body(node, ChunkContext::FunctionBody),
)),
"method_declaration" => Some(named_candidate(
node,
"fn",
ChunkKind::Function,
source,
recurse_body(node, ChunkContext::FunctionBody),
)),
@@ -45,7 +47,7 @@ impl LangClassifier for GoClassifier {
// ── Statements ──
"expression_statement" | "go_statement" | "defer_statement" | "send_statement" => {
Some(group_candidate(node, "stmts", source))
Some(group_candidate(node, ChunkKind::Statements, source))
},
_ => None,
@@ -55,17 +57,17 @@ impl LangClassifier for GoClassifier {
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
match node.kind() {
// ── Methods ──
"method_spec" => Some(named_candidate(node, "meth", source, None)),
"method_spec" => Some(named_candidate(node, ChunkKind::Method, source, None)),
// ── Fields ──
"field_declaration" | "embedded_field" => Some(match extract_identifier(node, source) {
Some(name) => make_named_chunk(node, format!("field_{name}"), source, None),
None => group_candidate(node, "fields", 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, "fields", source)),
"method_spec_list" => Some(group_candidate(node, "methods", source)),
"field_declaration_list" => Some(group_candidate(node, ChunkKind::Fields, source)),
"method_spec_list" => Some(group_candidate(node, ChunkKind::Methods, source)),
_ => None,
}
@@ -76,48 +78,48 @@ impl LangClassifier for GoClassifier {
// ── Control flow ──
"if_statement" => Some(make_candidate(
node,
"if".to_string(),
ChunkKind::If,
None,
NameStyle::Named,
None,
recurse_body(node, ChunkContext::FunctionBody),
false,
source,
)),
"switch_statement" | "expression_switch_statement" | "type_switch_statement" => {
Some(make_candidate(
node,
"switch".to_string(),
ChunkKind::Switch,
None,
NameStyle::Named,
None,
recurse_body(node, ChunkContext::FunctionBody),
false,
source,
))
},
"select_statement" => Some(make_candidate(
node,
"switch".to_string(),
ChunkKind::Switch,
None,
NameStyle::Named,
None,
recurse_body(node, ChunkContext::FunctionBody),
false,
source,
)),
// ── Loops ──
"for_statement" => Some(make_candidate(
node,
"for".to_string(),
ChunkKind::For,
None,
NameStyle::Named,
None,
recurse_body(node, ChunkContext::FunctionBody),
false,
source,
)),
// ── Blocks ──
"go_statement" | "defer_statement" | "send_statement" => {
Some(group_candidate(node, "stmts", source))
Some(group_candidate(node, ChunkKind::Statements, source))
},
// ── Variables ──
@@ -125,14 +127,12 @@ impl LangClassifier for GoClassifier {
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_named_chunk(node, format!("var_{name}"), source, None)
make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None)
} else {
let kind_name = sanitize_node_kind(node.kind());
group_candidate(node, &kind_name, source)
group_from_sanitized(node, source)
}
} else {
let kind_name = sanitize_node_kind(node.kind());
group_candidate(node, &kind_name, source)
group_from_sanitized(node, source)
})
},
@@ -147,30 +147,18 @@ fn classify_function_go<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'
let fn_recurse = || recurse_body(node, ChunkContext::FunctionBody);
match node.kind() {
"if_statement" => {
make_candidate(node, "if".to_string(), NameStyle::Named, None, fn_recurse(), false, source)
make_candidate(node, ChunkKind::If, None, NameStyle::Named, None, fn_recurse(), source)
},
"switch_statement"
| "expression_switch_statement"
| "type_switch_statement"
| "select_statement" => make_candidate(
node,
"switch".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"for_statement" => make_candidate(
node,
"for".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
_ => group_candidate(node, "stmts", source),
| "select_statement" => {
make_candidate(node, ChunkKind::Switch, None, NameStyle::Named, None, fn_recurse(), source)
},
"for_statement" => {
make_candidate(node, ChunkKind::For, None, NameStyle::Named, None, fn_recurse(), source)
},
_ => group_candidate(node, ChunkKind::Statements, source),
}
}
@@ -192,15 +180,22 @@ fn classify_type_decl<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t>
return make_container_chunk_from(
node,
spec,
format!("type_{name}"),
ChunkKind::Type,
Some(name),
source,
Some(recurse),
);
}
return make_named_chunk_from(node, spec, format!("type_{name}"), source, None);
return make_kind_chunk_from(node, spec, ChunkKind::Type, Some(name), source, None);
}
group_candidate(node, "decls", source)
group_candidate(node, ChunkKind::Declarations, source)
}
fn group_from_sanitized<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let kind_name = sanitize_node_kind(node.kind());
let kind = ChunkKind::from_sanitized_kind(kind_name.as_str());
make_candidate(node, kind, kind_name, NameStyle::Group, None, None, source)
}
/// For a `type_spec`, find a `struct_type` or `interface_type` child and return
+78 -50
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct GraphqlClassifier;
@@ -39,21 +39,21 @@ fn classify_graphql_root<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCan
match node.kind() {
"schema_definition" => Some(make_container_chunk(
node,
"schema".to_string(),
ChunkKind::Schema,
None,
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
)),
"directive_definition" => Some(make_container_chunk(
"directive_definition" => Some(make_named_graphql_chunk(
node,
format!(
"directive_{}",
extract_graphql_name(node, source).unwrap_or_else(|| "anonymous".to_string())
),
ChunkKind::Directive,
extract_graphql_name(node, source).unwrap_or_else(|| "anonymous".to_string()),
source,
recurse_into(node, ChunkContext::ClassBody, &[], &["arguments_definition"]),
)),
"scalar_type_definition" => Some(make_named_chunk(
"scalar_type_definition" => Some(make_named_graphql_chunk(
node,
ChunkKind::Type,
format!(
"scalar_{}",
extract_graphql_name(node, source).unwrap_or_else(|| "anonymous".to_string())
@@ -63,42 +63,35 @@ fn classify_graphql_root<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCan
)),
"object_type_definition" => Some(make_container_chunk(
node,
format!(
"type_{}",
extract_graphql_name(node, source).unwrap_or_else(|| "anonymous".to_string())
),
ChunkKind::Type,
extract_graphql_name(node, source),
source,
recurse_into(node, ChunkContext::ClassBody, &[], &["fields_definition"]),
)),
"interface_type_definition" => Some(make_container_chunk(
node,
format!(
"interface_{}",
extract_graphql_name(node, source).unwrap_or_else(|| "anonymous".to_string())
),
ChunkKind::Interface,
extract_graphql_name(node, source),
source,
recurse_into(node, ChunkContext::ClassBody, &[], &["fields_definition"]),
)),
"union_type_definition" => Some(make_named_chunk(
"union_type_definition" => Some(make_kind_chunk(
node,
format!(
"union_{}",
extract_graphql_name(node, source).unwrap_or_else(|| "anonymous".to_string())
),
ChunkKind::Union,
extract_graphql_name(node, source),
source,
None,
)),
"enum_type_definition" => Some(make_container_chunk(
node,
format!(
"enum_{}",
extract_graphql_name(node, source).unwrap_or_else(|| "anonymous".to_string())
),
ChunkKind::Enum,
extract_graphql_name(node, source),
source,
recurse_into(node, ChunkContext::ClassBody, &[], &["enum_values_definition"]),
)),
"input_object_type_definition" => Some(make_container_chunk(
"input_object_type_definition" => Some(make_named_graphql_chunk(
node,
ChunkKind::Type,
format!(
"input_{}",
extract_graphql_name(node, source).unwrap_or_else(|| "anonymous".to_string())
@@ -106,14 +99,16 @@ fn classify_graphql_root<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCan
source,
recurse_into(node, ChunkContext::ClassBody, &[], &["input_fields_definition"]),
)),
"operation_definition" => Some(make_container_chunk(
"operation_definition" => Some(make_named_graphql_chunk(
node,
ChunkKind::Operation,
extract_graphql_operation_chunk_name(node, source),
source,
recurse_into(node, ChunkContext::FunctionBody, &[], &["selection_set"]),
)),
"fragment_definition" => Some(make_container_chunk(
"fragment_definition" => Some(make_named_graphql_chunk(
node,
ChunkKind::Operation,
format!(
"fragment_{}",
extract_graphql_name(node, source).unwrap_or_else(|| "anonymous".to_string())
@@ -127,12 +122,10 @@ fn classify_graphql_root<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCan
fn classify_graphql_class<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
match node.kind() {
"root_operation_type_definition" => Some(make_named_chunk(
"root_operation_type_definition" => Some(make_named_graphql_chunk(
node,
format!(
"root_{}",
extract_graphql_operation_type(node, source).unwrap_or_else(|| "anonymous".to_string())
),
ChunkKind::Root,
extract_graphql_operation_type(node, source).unwrap_or_else(|| "anonymous".to_string()),
source,
None,
)),
@@ -141,14 +134,15 @@ fn classify_graphql_class<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCa
let recurse = recurse_into(node, ChunkContext::ClassBody, &[], &["arguments_definition"]);
Some(match recurse {
Some(recurse) => {
make_container_chunk(node, format!("field_{name}"), source, Some(recurse))
make_container_chunk(node, ChunkKind::Field, Some(name), source, Some(recurse))
},
None => make_named_chunk(node, format!("field_{name}"), source, None),
None => make_kind_chunk(node, ChunkKind::Field, Some(name), source, None),
})
},
"input_value_definition" => Some(classify_graphql_input_value(node, source)),
"enum_value_definition" => Some(make_named_chunk(
"enum_value_definition" => Some(make_named_graphql_chunk(
node,
ChunkKind::Variant,
format!(
"value_{}",
extract_graphql_name(node, source).unwrap_or_else(|| "anonymous".to_string())
@@ -177,16 +171,18 @@ fn classify_graphql_selection<'t>(node: Node<'t>, source: &str) -> Option<RawChu
Some(recurse) => make_container_chunk_from(
node,
child,
format!("field_{name}"),
ChunkKind::Field,
Some(name),
source,
Some(recurse),
),
None => make_named_chunk_from(node, child, format!("field_{name}"), source, None),
None => make_kind_chunk_from(node, child, ChunkKind::Field, Some(name), source, None),
})
},
"fragment_spread" => Some(make_named_chunk_from(
"fragment_spread" => Some(make_named_graphql_chunk_from(
node,
child,
ChunkKind::Operation,
format!(
"spread_{}",
extract_graphql_name(child, source).unwrap_or_else(|| "anonymous".to_string())
@@ -197,7 +193,8 @@ fn classify_graphql_selection<'t>(node: Node<'t>, source: &str) -> Option<RawChu
"inline_fragment" => Some(make_container_chunk_from(
node,
child,
"inline_fragment".to_string(),
ChunkKind::InlineFragment,
None,
source,
recurse_into(child, ChunkContext::FunctionBody, &[], &["selection_set"]),
)),
@@ -206,18 +203,49 @@ fn classify_graphql_selection<'t>(node: Node<'t>, source: &str) -> Option<RawChu
}
fn classify_graphql_input_value<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let prefix = match node.parent().map(|parent| parent.kind()) {
Some("input_fields_definition") => "field",
_ => "arg",
};
make_named_chunk(
let name = extract_graphql_name(node, source).unwrap_or_else(|| "anonymous".to_string());
match node.parent().map(|parent| parent.kind()) {
Some("input_fields_definition") => {
make_kind_chunk(node, ChunkKind::Field, Some(name), source, None)
},
_ => make_kind_chunk(node, ChunkKind::Arg, Some(name), source, None),
}
}
fn make_named_graphql_chunk<'t>(
node: Node<'t>,
kind: ChunkKind,
identifier: impl Into<Option<String>>,
source: &str,
recurse: Option<RecurseSpec<'t>>,
) -> RawChunkCandidate<'t> {
make_candidate(
node,
format!(
"{prefix}_{}",
extract_graphql_name(node, source).unwrap_or_else(|| "anonymous".to_string())
),
kind,
identifier,
NameStyle::Named,
signature_for_node(node, source),
recurse,
source,
)
}
fn make_named_graphql_chunk_from<'t>(
range_node: Node<'t>,
signature_node: Node<'t>,
kind: ChunkKind,
identifier: impl Into<Option<String>>,
source: &str,
recurse: Option<RecurseSpec<'t>>,
) -> RawChunkCandidate<'t> {
make_candidate(
range_node,
kind,
identifier,
NameStyle::Named,
signature_for_node(signature_node, source),
recurse,
source,
None,
)
}
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct HaskellScalaClassifier;
@@ -10,35 +10,49 @@ 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, "imports", source),
"package_declaration" => group_candidate(node, "imports", source),
"import_declaration" => group_candidate(node, ChunkKind::Imports, source),
"package_declaration" => group_candidate(node, ChunkKind::Imports, source),
// ── Haskell module ──
"module" => container_candidate(node, "mod", source, recurse_class(node)),
"module" => container_candidate(node, ChunkKind::Module, source, recurse_class(node)),
// ── Functions ──
"function_declaration" => {
named_candidate(node, "fn", source, recurse_body(node, ChunkContext::FunctionBody))
},
"function_definition" => {
named_candidate(node, "fn", source, recurse_body(node, ChunkContext::FunctionBody))
},
"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, "class", source, recurse_class(node)),
"object_definition" => container_candidate(node, "mod", source, recurse_class(node)),
"trait_definition" => container_candidate(node, "iface", source, recurse_interface(node)),
"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, "type", source, recurse_class(node))
named_candidate(node, ChunkKind::Type, source, recurse_class(node))
},
// ── Variables / assignments ──
"variable_declaration" | "assignment" => group_candidate(node, "decls", source),
"variable_declaration" | "assignment" => {
group_candidate(node, ChunkKind::Declarations, source)
},
// ── Statements ──
"expression_statement" => group_candidate(node, "stmts", source),
"expression_statement" => group_candidate(node, ChunkKind::Statements, source),
_ => return None,
})
@@ -50,16 +64,18 @@ impl LangClassifier for HaskellScalaClassifier {
"function_declaration" | "function_definition" | "method_definition" => {
let name = extract_identifier(node, source).unwrap_or_else(|| "anonymous".to_string());
if name == "constructor" {
make_named_chunk(
make_kind_chunk(
node,
"constructor".to_string(),
ChunkKind::Constructor,
None,
source,
recurse_body(node, ChunkContext::FunctionBody),
)
} else {
make_named_chunk(
make_kind_chunk(
node,
format!("fn_{name}"),
ChunkKind::Function,
Some(name),
source,
recurse_body(node, ChunkContext::FunctionBody),
)
@@ -69,8 +85,8 @@ impl LangClassifier for HaskellScalaClassifier {
// ── Fields ──
"variable_declaration" | "property_declaration" => {
match extract_identifier(node, source) {
Some(name) => make_named_chunk(node, format!("field_{name}"), source, None),
None => group_candidate(node, "fields", source),
Some(name) => make_kind_chunk(node, ChunkKind::Field, Some(name), source, None),
None => group_candidate(node, ChunkKind::Fields, source),
}
},
@@ -82,42 +98,36 @@ impl LangClassifier for HaskellScalaClassifier {
let fn_recurse = || recurse_body(node, ChunkContext::FunctionBody);
Some(match node.kind() {
// ── Control flow ──
"if_statement" => make_candidate(
node,
"if".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"if_statement" => {
make_candidate(node, ChunkKind::If, None, NameStyle::Named, None, fn_recurse(), source)
},
"match_expression" => make_candidate(
node,
"match".to_string(),
ChunkKind::Match,
None,
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"for_expression" | "while_expression" => make_candidate(
node,
"loop".to_string(),
ChunkKind::Loop,
None,
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
// ── Blocks ──
"block_expression" => make_candidate(
node,
"block".to_string(),
ChunkKind::Block,
None,
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
+4 -3
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct HtmlXmlClassifier;
@@ -20,12 +20,13 @@ fn classify_element<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidat
let recurse_target = child_by_kind(node, &["content"]).unwrap_or(node);
Some(make_container_chunk(
node,
format!("tag_{tag_name}"),
ChunkKind::Tag,
Some(tag_name),
source,
Some(recurse_self(recurse_target, ChunkContext::ClassBody)),
))
},
"text_node" => Some(group_candidate(node, "text", source)),
"text_node" => Some(group_candidate(node, ChunkKind::Text, source)),
_ => None,
}
}
+9 -4
View File
@@ -7,7 +7,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct IniClassifier;
@@ -25,20 +25,25 @@ fn classify_ini_root<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandida
Some(match node.kind() {
"section" => make_container_chunk(
node,
format!("section_{}", ini_name(node, source)?),
ChunkKind::Section,
Some(ini_name(node, source)?),
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
),
// INI permits settings before any section header; keep them as first-class
// chunks instead of forcing them under a synthetic container.
"setting" => make_named_chunk(node, format!("key_{}", ini_name(node, source)?), source, None),
"setting" => {
make_kind_chunk(node, ChunkKind::Key, Some(ini_name(node, source)?), source, None)
},
_ => return None,
})
}
fn classify_ini_class<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
Some(match node.kind() {
"setting" => make_named_chunk(node, format!("key_{}", ini_name(node, source)?), source, None),
"setting" => {
make_kind_chunk(node, ChunkKind::Key, Some(ini_name(node, source)?), source, None)
},
_ => return None,
})
}
+10 -8
View File
@@ -26,7 +26,9 @@ use serde::Serialize;
use serde_json::{Map, Value};
use crate::chunk::{
build_chunk_tree, chunk_checksum, line_start_offsets,
build_chunk_tree, chunk_checksum,
kind::ChunkKind,
line_start_offsets,
types::{ChunkNode, ChunkTree},
};
@@ -395,7 +397,8 @@ pub fn build_notebook_tree_from_virtual(
let mut chunks: Vec<ChunkNode> = Vec::with_capacity(1 + regions.len() * 2);
chunks.push(ChunkNode {
path: String::new(),
name: "root".to_string(),
identifier: None,
kind: ChunkKind::Root,
leaf: false,
parent_path: None,
children: Vec::new(),
@@ -437,7 +440,6 @@ pub fn build_notebook_tree_from_virtual(
if body_has_content {
let sub_tree = build_chunk_tree(body, cell_language_str.as_str())
.map_err(|err| format!("Failed to parse cell_{} body: {err}", region.cell_num))?;
// sub_tree.chunks[0] is the sub-root; skip it.
for sub_chunk in sub_tree.chunks.into_iter().skip(1) {
let translated_path = format!("{}.{}", cell_path, sub_chunk.path);
let translated_parent = match sub_chunk.parent_path.as_deref() {
@@ -458,7 +460,8 @@ pub fn build_notebook_tree_from_virtual(
let line_shift = region.content_line.saturating_sub(1);
chunks.push(ChunkNode {
path: translated_path.clone(),
name: sub_chunk.name,
identifier: sub_chunk.identifier,
kind: sub_chunk.kind,
leaf: sub_chunk.leaf,
parent_path: translated_parent,
children: translated_children,
@@ -484,13 +487,11 @@ pub fn build_notebook_tree_from_virtual(
group: false,
});
}
// Collect direct children (those whose parent_path equals cell_path).
for sub_path in sub_tree.root_children {
cell_children_paths.push(format!("{cell_path}.{sub_path}"));
}
}
// Cell parent chunk: spans from the marker line through the body.
let cell_line_count = {
let body_lines = if body_has_content {
if body.ends_with('\n') {
@@ -501,13 +502,14 @@ pub fn build_notebook_tree_from_virtual(
} else {
0
};
1 + body_lines as u32 // +1 for the marker line
1 + body_lines as u32
};
let cell_end_line = region.marker_line + cell_line_count.saturating_sub(1);
let cell_leaf = cell_children_paths.is_empty();
chunks.push(ChunkNode {
path: cell_path.clone(),
name: cell_path.clone(),
identifier: Some(cell_path.clone()),
kind: ChunkKind::Cell,
leaf: cell_leaf,
parent_path: Some(String::new()),
children: cell_children_paths,
+123 -67
View File
@@ -2,7 +2,9 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*, defaults::promote_assigned_expression};
use super::{
classify::LangClassifier, common::*, defaults::promote_assigned_expression, kind::ChunkKind,
};
pub struct JsTsClassifier;
@@ -19,7 +21,7 @@ impl LangClassifier for JsTsClassifier {
// ── Imports ──
"import_statement" | "import_declaration" => {
Some(group_candidate(node, "imports", source))
Some(group_candidate(node, ChunkKind::Imports, source))
},
// ── Variables ──
@@ -33,25 +35,30 @@ impl LangClassifier for JsTsClassifier {
// ── Functions ──
"function_declaration" => Some(named_candidate(
node,
"fn",
ChunkKind::Function,
source,
recurse_body(node, ChunkContext::FunctionBody),
)),
// ── Containers ──
"class_declaration" => {
Some(container_candidate(node, "class", source, recurse_class(node)))
Some(container_candidate(node, ChunkKind::Class, source, recurse_class(node)))
},
"interface_declaration" => {
Some(container_candidate(node, "interface", source, recurse_interface(node)))
Some(container_candidate(node, ChunkKind::Interface, source, recurse_interface(node)))
},
"enum_declaration" => Some(container_candidate(node, "enum", source, recurse_enum(node))),
"internal_module" => {
Some(container_candidate(node, "mod", source, recurse_internal_module(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, "type", source, None)),
"type_alias_declaration" => Some(named_candidate(node, ChunkKind::Type, source, None)),
// ── Control flow at top level ──
"if_statement" | "switch_statement" | "switch_expression" | "try_statement"
@@ -65,9 +72,9 @@ impl LangClassifier for JsTsClassifier {
.into_iter()
.find(|c| c.kind() == "internal_module");
if let Some(ns) = inner {
Some(container_candidate(ns, "mod", source, recurse_internal_module(ns)))
Some(container_candidate(ns, ChunkKind::Module, source, recurse_internal_module(ns)))
} else {
Some(group_candidate(node, "stmts", source))
Some(group_candidate(node, ChunkKind::Statements, source))
}
},
@@ -85,9 +92,10 @@ impl LangClassifier for JsTsClassifier {
"lexical_declaration" | "variable_declaration" => Some(classify_var_decl_js(node, source)),
// ── Constructor ──
"constructor" => Some(make_named_chunk(
"constructor" => Some(make_kind_chunk(
node,
"constructor".to_string(),
ChunkKind::Constructor,
None,
source,
recurse_body(node, ChunkContext::FunctionBody),
)),
@@ -96,16 +104,18 @@ impl LangClassifier for JsTsClassifier {
"method_definition" | "method_signature" | "abstract_method_signature" => {
let name = extract_identifier(node, source).unwrap_or_else(|| "anonymous".to_string());
if name == "constructor" {
Some(make_named_chunk(
Some(make_kind_chunk(
node,
"constructor".to_string(),
ChunkKind::Constructor,
None,
source,
recurse_body(node, ChunkContext::FunctionBody),
))
} else {
Some(make_named_chunk(
Some(make_kind_chunk(
node,
format!("fn_{name}"),
ChunkKind::Function,
Some(name),
source,
recurse_body(node, ChunkContext::FunctionBody),
))
@@ -119,23 +129,23 @@ impl LangClassifier for JsTsClassifier {
| "property_signature"
| "property_declaration"
| "abstract_class_field" => match extract_identifier(node, source) {
Some(name) => Some(make_named_chunk(node, format!("field_{name}"), source, None)),
None => Some(group_candidate(node, "fields", 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_named_chunk(node, format!("variant_{name}"), source, None)),
None => Some(group_candidate(node, "variants", 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_named_chunk(node, "static_init".to_string(), source, None))
Some(make_kind_chunk(node, ChunkKind::StaticInit, None, source, None))
},
// ── Types ──
"type_alias_declaration" => Some(named_candidate(node, "type", source, None)),
"type_alias_declaration" => Some(named_candidate(node, ChunkKind::Type, source, None)),
_ => None,
}
@@ -153,41 +163,54 @@ fn classify_function_js<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'
let fn_recurse = || recurse_body(node, ChunkContext::FunctionBody);
match node.kind() {
// ── Control flow ──
"if_statement" => make_named_chunk(node, "if".to_string(), source, fn_recurse()),
"switch_statement" | "switch_expression" => {
make_named_chunk(node, "switch".to_string(), source, fn_recurse())
"if_statement" => {
make_candidate(node, ChunkKind::If, None, NameStyle::Named, None, fn_recurse(), source)
},
"switch_statement" | "switch_expression" => {
make_candidate(node, ChunkKind::Switch, None, NameStyle::Named, None, fn_recurse(), source)
},
"try_statement" => {
make_candidate(node, ChunkKind::Try, None, NameStyle::Named, None, fn_recurse(), source)
},
"try_statement" => make_named_chunk(node, "try".to_string(), source, fn_recurse()),
// ── Loops ──
"for_statement" => make_named_chunk(node, "for".to_string(), source, fn_recurse()),
"for_in_statement" => make_named_chunk(node, "for_in".to_string(), source, fn_recurse()),
"for_of_statement" => make_named_chunk(node, "for_of".to_string(), source, fn_recurse()),
"while_statement" => make_named_chunk(node, "while".to_string(), source, fn_recurse()),
"do_statement" => make_named_chunk(node, "block".to_string(), source, fn_recurse()),
"for_statement" => {
make_candidate(node, ChunkKind::For, None, NameStyle::Named, None, fn_recurse(), source)
},
"for_in_statement" => {
make_candidate(node, ChunkKind::ForIn, None, NameStyle::Named, None, fn_recurse(), source)
},
"for_of_statement" => {
make_candidate(node, ChunkKind::ForOf, None, NameStyle::Named, None, fn_recurse(), source)
},
"while_statement" => {
make_candidate(node, ChunkKind::While, None, NameStyle::Named, None, fn_recurse(), source)
},
"do_statement" => {
make_candidate(node, ChunkKind::Block, None, NameStyle::Named, None, fn_recurse(), source)
},
// ── Blocks ──
"with_statement" => make_named_chunk(node, "block".to_string(), source, fn_recurse()),
"with_statement" => {
make_candidate(node, ChunkKind::Block, None, NameStyle::Named, None, fn_recurse(), source)
},
// ── Variables ──
"lexical_declaration" | "variable_declaration" => {
let span = line_span(node.start_position().row + 1, node.end_position().row + 1);
if span > 1 {
if let Some(name) = extract_single_declarator_name(node, source) {
make_named_chunk(node, format!("var_{name}"), source, None)
make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None)
} else {
group_candidate(node, &sanitize_node_kind(node.kind()), source)
group_from_sanitized(node, source)
}
} else {
group_candidate(node, &sanitize_node_kind(node.kind()), source)
group_from_sanitized(node, source)
}
},
// ── Fallback ──
_ => {
let kind_name = sanitize_node_kind(node.kind());
group_candidate(node, &kind_name, source)
},
_ => group_from_sanitized(node, source),
}
}
@@ -209,11 +232,11 @@ fn classify_var_decl_js<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'
match value.kind() {
"arrow_function" | "function_expression" | "function" => {
let recurse = recurse_body(value, ChunkContext::FunctionBody);
return make_named_chunk(node, format!("fn_{name}"), source, recurse);
return make_kind_chunk(node, ChunkKind::Function, Some(name), source, recurse);
},
"class" | "class_expression" => {
let recurse = recurse_class(value);
return make_container_chunk(node, format!("class_{name}"), source, recurse);
return make_container_chunk(node, ChunkKind::Class, Some(name), source, recurse);
},
_ => {},
}
@@ -221,9 +244,15 @@ fn classify_var_decl_js<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'
}
// Not promoted — fall back to var_NAME or group.
if let Some(name) = extract_single_declarator_name(node, source) {
return make_named_chunk(node, format!("var_{name}"), source, None);
return make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None);
}
group_candidate(node, "decls", source)
group_candidate(node, ChunkKind::Declarations, source)
}
fn group_from_sanitized<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let kind_name = sanitize_node_kind(node.kind());
let kind = ChunkKind::from_sanitized_kind(kind_name.as_str());
make_candidate(node, kind, kind_name, NameStyle::Group, None, None, source)
}
// ── Export statement ─────────────────────────────────────────────────────
@@ -244,9 +273,9 @@ fn classify_export_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandid
let Some(child) = inner else {
// `export { foo } from "bar"` with no inner declaration node.
return if is_default {
make_named_chunk(node, "default_export".to_string(), source, None)
make_kind_chunk(node, ChunkKind::DefaultExport, None, source, None)
} else {
group_candidate(node, "stmts", source)
group_candidate(node, ChunkKind::Statements, source)
};
};
@@ -254,34 +283,48 @@ fn classify_export_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandid
"class_declaration" => {
let recurse = recurse_class(child);
if is_default {
make_container_chunk_from(node, child, "default_export".to_string(), source, recurse)
make_container_chunk_from(node, child, ChunkKind::DefaultExport, None, source, recurse)
} else {
make_container_chunk_from(
node,
child,
prefixed_name("class", child, source),
ChunkKind::Class,
extract_identifier(child, source),
source,
recurse,
)
}
},
"function_declaration" => {
"function_declaration"
| "function"
| "function_expression"
| "arrow_function"
| "generator_function"
| "generator_function_declaration" => {
let recurse = recurse_body(child, ChunkContext::FunctionBody);
if is_default {
make_named_chunk_from(node, child, "default_export".to_string(), source, recurse)
make_kind_chunk_from(node, child, ChunkKind::DefaultExport, None, source, recurse)
} else {
make_named_chunk_from(node, child, prefixed_name("fn", child, source), source, recurse)
make_kind_chunk_from(
node,
child,
ChunkKind::Function,
extract_identifier(child, source),
source,
recurse,
)
}
},
"interface_declaration" => {
let recurse = recurse_interface(child);
if is_default {
make_container_chunk_from(node, child, "default_export".to_string(), source, recurse)
make_container_chunk_from(node, child, ChunkKind::DefaultExport, None, source, recurse)
} else {
make_container_chunk_from(
node,
child,
prefixed_name("interface", child, source),
ChunkKind::Interface,
extract_identifier(child, source),
source,
recurse,
)
@@ -289,20 +332,28 @@ fn classify_export_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandid
},
"type_alias_declaration" => {
if is_default {
make_named_chunk_from(node, child, "default_export".to_string(), source, None)
make_kind_chunk_from(node, child, ChunkKind::DefaultExport, None, source, None)
} else {
make_named_chunk_from(node, child, prefixed_name("type", child, source), source, None)
make_kind_chunk_from(
node,
child,
ChunkKind::Type,
extract_identifier(child, source),
source,
None,
)
}
},
"enum_declaration" => {
let recurse = recurse_enum(child);
if is_default {
make_container_chunk_from(node, child, "default_export".to_string(), source, recurse)
make_container_chunk_from(node, child, ChunkKind::DefaultExport, None, source, recurse)
} else {
make_container_chunk_from(
node,
child,
prefixed_name("enum", child, source),
ChunkKind::Enum,
extract_identifier(child, source),
source,
recurse,
)
@@ -311,12 +362,13 @@ fn classify_export_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandid
"internal_module" => {
let recurse = recurse_internal_module(child);
if is_default {
make_container_chunk_from(node, child, "default_export".to_string(), source, recurse)
make_container_chunk_from(node, child, ChunkKind::DefaultExport, None, source, recurse)
} else {
make_container_chunk_from(
node,
child,
prefixed_name("mod", child, source),
ChunkKind::Module,
extract_identifier(child, source),
source,
recurse,
)
@@ -324,7 +376,7 @@ fn classify_export_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandid
},
"lexical_declaration" | "variable_declaration" => {
if is_default {
make_named_chunk_from(node, child, "default_export".to_string(), source, None)
make_kind_chunk_from(node, child, ChunkKind::DefaultExport, None, source, None)
} else {
promote_assigned_expression(node, child, source)
.unwrap_or_else(|| super::defaults::classify_var_decl(child, source))
@@ -333,16 +385,14 @@ fn classify_export_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandid
_ => {
// expression_statement, re-exports, or anything else.
if is_default {
make_named_chunk_from(node, child, "default_export".to_string(), source, None)
make_kind_chunk_from(node, child, ChunkKind::DefaultExport, None, source, None)
} else {
group_candidate(child, "stmts", source)
group_candidate(child, ChunkKind::Statements, source)
}
},
}
}
// ── Decorated definition ─────────────────────────────────────────────────
/// Unwrap `@decorator` wrappers (TS/Python `decorated_definition`) to find
/// the inner class or function definition.
fn classify_decorated<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
@@ -356,16 +406,22 @@ fn classify_decorated<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t>
match inner {
Some(child) if child.kind() == "class_declaration" || child.kind() == "class_definition" => {
let recurse = recurse_class(child);
make_container_chunk(node, prefixed_name("class", child, source), source, recurse)
make_container_chunk(
node,
ChunkKind::Class,
extract_identifier(child, source),
source,
recurse,
)
},
Some(child) => {
// function_declaration | function_definition
let name = extract_identifier(child, source).unwrap_or_else(|| "anonymous".to_string());
make_named_chunk(node, format!("fn_{name}"), source, {
make_kind_chunk(node, ChunkKind::Function, Some(name), source, {
let context = ChunkContext::FunctionBody;
recurse_into(child, context, &["body"], &["block"])
})
},
None => positional_candidate(node, "block", source),
None => positional_candidate(node, ChunkKind::Block, source),
}
}
+7 -6
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct JustClassifier;
@@ -42,17 +42,18 @@ fn classify_just_root_node<'t>(node: Node<'t>, source: &str) -> Option<RawChunkC
Some(match node.kind() {
"setting" => {
let name = extract_setting_name(node, source).unwrap_or_else(|| "anonymous".to_string());
make_named_chunk(node, format!("setting_{name}"), source, None)
make_kind_chunk(node, ChunkKind::Setting, Some(name), source, None)
},
"alias" => {
let name = extract_alias_name(node, source).unwrap_or_else(|| "anonymous".to_string());
make_named_chunk(node, format!("alias_{name}"), source, None)
make_kind_chunk(node, ChunkKind::Alias, Some(name), source, None)
},
"recipe" => {
let name = extract_recipe_name(node, source).unwrap_or_else(|| "anonymous".to_string());
make_container_chunk(
node,
format!("recipe_{name}"),
ChunkKind::Recipe,
Some(name),
source,
recurse_into(node, ChunkContext::FunctionBody, &[], &["recipe_body"]),
)
@@ -65,8 +66,8 @@ fn classify_just_body_node<'t>(node: Node<'t>, source: &str) -> Option<RawChunkC
Some(match node.kind() {
// Just recipe bodies are line-oriented; tree-sitter exposes shell lines as
// `recipe_line` leaves rather than a nested shell AST.
"recipe_line" => group_candidate(node, "cmd", source),
"shebang" => make_named_chunk(node, "shebang".to_string(), source, None),
"recipe_line" => group_candidate(node, ChunkKind::Cmd, source),
"shebang" => make_kind_chunk(node, ChunkKind::Shebang, None, source, None),
_ => return None,
})
}
+20 -12
View File
@@ -2,36 +2,38 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct MarkupClassifier;
impl MarkupClassifier {
fn classify_section<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name = extract_markdown_heading(node, source).unwrap_or_else(|| "anonymous".to_string());
make_container_chunk(
force_container(make_container_chunk(
node,
format!("section_{name}"),
ChunkKind::Section,
Some(name),
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
)
))
}
fn classify_block_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name =
extract_glimmer_block_name(node, source).unwrap_or_else(|| "anonymous".to_string());
make_container_chunk(
force_container(make_container_chunk(
node,
format!("block_{name}"),
ChunkKind::Block,
Some(name),
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
)
))
}
fn classify_mustache_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name =
extract_glimmer_mustache_name(node, source).unwrap_or_else(|| "anonymous".to_string());
make_named_chunk(node, format!("mustache_{name}"), source, None)
make_kind_chunk(node, ChunkKind::Mustache, Some(name), source, None)
}
/// Classify HTML-like element nodes that appear inside handlebars blocks.
@@ -40,14 +42,15 @@ impl MarkupClassifier {
"element" | "script_element" | "style_element" | "element_node" => {
let name =
extract_element_tag_name(node, source).unwrap_or_else(|| "anonymous".to_string());
Some(make_container_chunk(
Some(force_container(make_container_chunk(
node,
format!("tag_{name}"),
ChunkKind::Tag,
Some(name),
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
))
)))
},
"text_node" => Some(group_candidate(node, "text", source)),
"text_node" => Some(group_candidate(node, ChunkKind::Text, source)),
_ => None,
}
}
@@ -87,6 +90,11 @@ impl LangClassifier for MarkupClassifier {
}
}
const fn force_container(mut candidate: RawChunkCandidate<'_>) -> RawChunkCandidate<'_> {
candidate.force_recurse = true;
candidate
}
/// Extract heading text from a Markdown `section` node's `atx_heading` or
/// `setext_heading` child.
fn extract_markdown_heading(node: Node<'_>, source: &str) -> Option<String> {
+110 -104
View File
@@ -6,10 +6,23 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*, defaults::classify_var_decl};
use super::{classify::LangClassifier, common::*, defaults::classify_var_decl, kind::ChunkKind};
pub struct MiscClassifier;
fn sanitized_group_candidate<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let sanitized = sanitize_node_kind(node.kind());
let kind = ChunkKind::from_sanitized_kind(sanitized.as_str());
// For unknown kinds that fall back to `Chunk`, preserve the original
// tree-sitter kind as the identifier so the path stays informative.
let identifier = if kind == ChunkKind::Chunk {
Some(sanitized)
} else {
None
};
make_candidate(node, kind, identifier, NameStyle::Group, None, None, source)
}
impl LangClassifier for MiscClassifier {
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
let fn_recurse = || {
@@ -37,21 +50,21 @@ impl LangClassifier for MiscClassifier {
| "import_list"
| "import_header"
| "package_header"
| "package_declaration" => group_candidate(node, "imports", source),
| "package_declaration" => group_candidate(node, ChunkKind::Imports, source),
// ── Variables / assignments ──
"lexical_declaration" | "variable_declaration" => classify_var_decl(node, source),
"const_declaration" | "var_declaration" => match extract_identifier(node, source) {
Some(name) => make_named_chunk(node, format!("var_{name}"), source, None),
None => group_candidate(node, "decls", source),
Some(name) => make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None),
None => group_candidate(node, ChunkKind::Declarations, source),
},
"assignment" | "property_declaration" | "state_variable_declaration" => {
group_candidate(node, "decls", source)
group_candidate(node, ChunkKind::Declarations, source)
},
// ── Statements ──
"expression_statement" | "global_statement" | "command" | "pipeline" | "function_call" => {
group_candidate(node, "stmts", source)
group_candidate(node, ChunkKind::Statements, source)
},
// ── Functions ──
@@ -59,61 +72,74 @@ impl LangClassifier for MiscClassifier {
| "function_definition"
| "procedure_declaration"
| "overloaded_procedure_declaration"
| "test_declaration" => named_candidate(node, "fn", source, fn_recurse()),
"method_declaration" => {
named_candidate(node, "meth", source, recurse_body(node, ChunkContext::FunctionBody))
},
| "test_declaration" => named_candidate(node, ChunkKind::Function, source, fn_recurse()),
"method_declaration" => named_candidate(
node,
ChunkKind::Method,
source,
recurse_body(node, ChunkContext::FunctionBody),
),
"constructor_definition"
| "constructor_declaration"
| "secondary_constructor"
| "init_declaration"
| "fallback_receive_definition" => make_named_chunk(
| "fallback_receive_definition" => make_kind_chunk(
node,
"constructor".to_string(),
ChunkKind::Constructor,
None,
source,
recurse_body(node, ChunkContext::FunctionBody),
),
// ── Containers ──
"class_declaration" | "class_definition" => {
container_candidate(node, "class", source, recurse_class(node))
container_candidate(node, ChunkKind::Class, source, recurse_class(node))
},
"interface_declaration" | "protocol_declaration" => {
container_candidate(node, "iface", source, recurse_interface(node))
container_candidate(node, ChunkKind::Iface, source, recurse_interface(node))
},
"struct_declaration" | "object_declaration" => {
container_candidate(node, "struct", source, recurse_class(node))
container_candidate(node, ChunkKind::Struct, source, recurse_class(node))
},
"enum_declaration" | "enum_definition" => {
container_candidate(node, "enum", source, recurse_enum(node))
container_candidate(node, ChunkKind::Enum, source, recurse_enum(node))
},
"trait_definition" | "class" => {
container_candidate(node, "trait", source, recurse_class(node))
container_candidate(node, ChunkKind::Trait, source, recurse_class(node))
},
"contract_declaration" | "library_declaration" | "trait_declaration" => {
container_candidate(node, "contract", source, recurse_class(node))
container_candidate(node, ChunkKind::Contract, source, recurse_class(node))
},
"namespace_declaration"
| "namespace_definition"
| "module_definition"
| "extension_definition" => container_candidate(node, "mod", source, module_recurse()),
| "extension_definition" => {
container_candidate(node, ChunkKind::Module, source, module_recurse())
},
// ── Types / aliases ──
"type_alias_declaration" | "const_type_declaration" | "opaque_declaration" => {
named_candidate(node, "type", source, recurse_class(node))
named_candidate(node, ChunkKind::Type, source, recurse_class(node))
},
// ── Macros ──
"macro_definition" | "modifier_definition" => {
named_candidate(node, "macro", source, recurse_body(node, ChunkContext::FunctionBody))
},
"macro_definition" | "modifier_definition" => named_candidate(
node,
ChunkKind::Macro,
source,
recurse_body(node, ChunkContext::FunctionBody),
),
// ── Systems (Verilog etc.) ──
"covergroup_declaration" | "checker_declaration" => {
container_candidate(node, "group", source, recurse_class(node))
container_candidate(node, ChunkKind::Group, source, recurse_class(node))
},
"module_declaration" => {
container_candidate(node, ChunkKind::Module, source, recurse_class(node))
},
"union_declaration" => {
container_candidate(node, ChunkKind::Union, source, recurse_class(node))
},
"module_declaration" => container_candidate(node, "mod", source, recurse_class(node)),
"union_declaration" => container_candidate(node, "union", source, recurse_class(node)),
// ── Control flow at top level → delegate to function-level ──
"if_statement"
@@ -145,9 +171,10 @@ impl LangClassifier for MiscClassifier {
"constructor"
| "constructor_declaration"
| "secondary_constructor"
| "init_declaration" => make_named_chunk(
| "init_declaration" => make_kind_chunk(
node,
"constructor".to_string(),
ChunkKind::Constructor,
None,
source,
recurse_body(node, ChunkContext::FunctionBody),
),
@@ -166,16 +193,18 @@ impl LangClassifier for MiscClassifier {
| "singleton_method" => {
let name = extract_identifier(node, source).unwrap_or_else(|| "anonymous".to_string());
if name == "constructor" {
make_named_chunk(
make_kind_chunk(
node,
"constructor".to_string(),
ChunkKind::Constructor,
None,
source,
recurse_body(node, ChunkContext::FunctionBody),
)
} else {
make_named_chunk(
make_kind_chunk(
node,
format!("fn_{name}"),
ChunkKind::Function,
Some(name),
source,
recurse_body(node, ChunkContext::FunctionBody),
)
@@ -193,8 +222,8 @@ impl LangClassifier for MiscClassifier {
| "const_declaration"
| "constant_declaration"
| "event_field_declaration" => match extract_identifier(node, source) {
Some(name) => make_named_chunk(node, format!("field_{name}"), source, None),
None => group_candidate(node, "fields", source),
Some(name) => make_kind_chunk(node, ChunkKind::Field, Some(name), source, None),
None => group_candidate(node, ChunkKind::Fields, source),
},
// ── Enum variants ──
@@ -203,27 +232,27 @@ impl LangClassifier for MiscClassifier {
| "enum_constant"
| "enum_entry"
| "enum_variant" => match extract_identifier(node, source) {
Some(name) => make_named_chunk(node, format!("variant_{name}"), source, None),
None => group_candidate(node, "variants", source),
Some(name) => make_kind_chunk(node, ChunkKind::Variant, Some(name), source, None),
None => group_candidate(node, ChunkKind::Variants, source),
},
// ── Other fields ──
"field_declaration" | "embedded_field" | "container_field" | "binding" => {
match extract_identifier(node, source) {
Some(name) => make_named_chunk(node, format!("field_{name}"), source, None),
None => group_candidate(node, "fields", source),
Some(name) => make_kind_chunk(node, ChunkKind::Field, Some(name), source, None),
None => group_candidate(node, ChunkKind::Fields, source),
}
},
// ── Method specs ──
"method_spec" => named_candidate(node, "meth", source, None),
"method_spec" => named_candidate(node, ChunkKind::Method, source, None),
// ── Field / method lists ──
"field_declaration_list" => group_candidate(node, "fields", source),
"method_spec_list" => group_candidate(node, "methods", source),
"field_declaration_list" => group_candidate(node, ChunkKind::Fields, source),
"method_spec_list" => group_candidate(node, ChunkKind::Methods, source),
// ── Static initializer ──
"class_static_block" => make_named_chunk(node, "static_init".to_string(), source, None),
"class_static_block" => make_kind_chunk(node, ChunkKind::StaticInit, None, source, None),
// ── Decorated definitions ──
"decorated_definition" => {
@@ -233,7 +262,7 @@ impl LangClassifier for MiscClassifier {
if let Some(child) = inner {
let name =
extract_identifier(child, source).unwrap_or_else(|| "anonymous".to_string());
make_named_chunk(node, format!("fn_{name}"), source, {
make_kind_chunk(node, ChunkKind::Function, Some(name), source, {
let context = ChunkContext::FunctionBody;
recurse_into(child, context, &["body"], &["block"])
})
@@ -248,15 +277,15 @@ impl LangClassifier for MiscClassifier {
| "attribute"
| "pair"
| "block_mapping_pair"
| "flow_pair" => group_candidate(node, "fields", source),
| "flow_pair" => group_candidate(node, ChunkKind::Fields, source),
// ── Types inside classes ──
"type_item" | "type_alias_declaration" | "type_alias" => {
named_candidate(node, "type", source, None)
named_candidate(node, ChunkKind::Type, source, None)
},
// ── Const / macro inside classes ──
"const_item" | "macro_invocation" => group_candidate(node, "fields", source),
"const_item" | "macro_invocation" => group_candidate(node, ChunkKind::Fields, source),
_ => return None,
})
@@ -266,15 +295,9 @@ impl LangClassifier for MiscClassifier {
let fn_recurse = || recurse_body(node, ChunkContext::FunctionBody);
Some(match node.kind() {
// ── Control flow: conditionals ──
"if_statement" | "unless" | "guard_statement" => make_candidate(
node,
"if".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"if_statement" | "unless" | "guard_statement" => {
make_candidate(node, ChunkKind::If, None, NameStyle::Named, None, fn_recurse(), source)
},
// ── Control flow: switches ──
"switch_statement"
@@ -287,87 +310,72 @@ impl LangClassifier for MiscClassifier {
| "receive_statement"
| "yul_switch_statement" => make_candidate(
node,
"switch".to_string(),
ChunkKind::Switch,
None,
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
// ── Control flow: try/catch ──
"try_statement" | "try_block" | "catch_clause" | "finally_clause"
| "assembly_statement" => make_candidate(
node,
"try".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
| "assembly_statement" => {
make_candidate(node, ChunkKind::Try, None, NameStyle::Named, None, fn_recurse(), source)
},
// ── Loops: for variants (with Python-like check) ──
"for_statement" | "for_in_statement" | "for_of_statement" => {
let name = if looks_like_python_statement(node, source) {
"loop".to_string()
let kind = if looks_like_python_statement(node, source) {
ChunkKind::Loop
} else {
sanitize_node_kind(node.kind())
match node.kind() {
"for_statement" => ChunkKind::For,
"for_in_statement" => ChunkKind::ForIn,
"for_of_statement" => ChunkKind::ForOf,
_ => unreachable!(),
}
};
make_candidate(node, name, NameStyle::Named, None, fn_recurse(), false, source)
make_candidate(node, kind, None, NameStyle::Named, None, fn_recurse(), source)
},
// ── Loops: while ──
"while_statement" => {
let name = if looks_like_python_statement(node, source) {
"loop"
let kind = if looks_like_python_statement(node, source) {
ChunkKind::Loop
} else {
"while"
ChunkKind::While
};
make_candidate(
node,
name.to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
)
make_candidate(node, kind, None, NameStyle::Named, None, fn_recurse(), source)
},
// ── Blocks ──
"do_statement" | "with_statement" | "do_block" | "subshell" | "async_block"
| "unsafe_block" | "const_block" | "block_expression" => make_candidate(
node,
"block".to_string(),
ChunkKind::Block,
None,
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
// ── Loops: foreach ──
"foreach_statement" => make_candidate(
node,
"for".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"foreach_statement" => {
make_candidate(node, ChunkKind::For, None, NameStyle::Named, None, fn_recurse(), source)
},
// ── Statements ──
"defer_statement" | "go_statement" | "send_statement" => {
group_candidate(node, "stmts", source)
group_candidate(node, ChunkKind::Statements, source)
},
// ── Positional candidates ──
"elif_clause" => positional_candidate(node, "elif", source),
"except_clause" => positional_candidate(node, "except", source),
"when_statement" => positional_candidate(node, "when", source),
"match_expression" | "match_block" => positional_candidate(node, "match", source),
"elif_clause" => positional_candidate(node, ChunkKind::Elif, source),
"except_clause" => positional_candidate(node, ChunkKind::Except, source),
"when_statement" => positional_candidate(node, ChunkKind::When, source),
"match_expression" | "match_block" => positional_candidate(node, ChunkKind::Match, source),
// ── Loops / misc expressions ──
"loop_expression"
@@ -378,7 +386,7 @@ impl LangClassifier for MiscClassifier {
| "nosuspend_statement"
| "suspend_statement"
| "yul_if_statement"
| "yul_for_statement" => positional_candidate(node, "loop", source),
| "yul_for_statement" => positional_candidate(node, ChunkKind::Loop, source),
// ── Variable declarations ──
"lexical_declaration"
@@ -390,14 +398,12 @@ impl LangClassifier for MiscClassifier {
let span = line_span(node.start_position().row + 1, node.end_position().row + 1);
if span > 1 {
if let Some(name) = extract_single_declarator_name(node, source) {
make_named_chunk(node, format!("var_{name}"), source, None)
make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None)
} else {
let kind_name = sanitize_node_kind(node.kind());
group_candidate(node, &kind_name, source)
sanitized_group_candidate(node, source)
}
} else {
let kind_name = sanitize_node_kind(node.kind());
group_candidate(node, &kind_name, source)
sanitized_group_candidate(node, source)
}
},
+50 -23
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct NixHclClassifier;
@@ -59,16 +59,21 @@ fn recurse_nix_binding_value(node: Node<'_>) -> Option<RecurseSpec<'_>> {
fn classify_nix_binding<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name = extract_nix_binding_name(node, source).unwrap_or_else(|| "anonymous".to_string());
let chunk_name = format!("attr_{name}");
let expression = node.child_by_field_name("expression");
if let Some(expression) = expression
&& matches!(
expression.kind(),
"attrset_expression" | "let_attrset_expression" | "rec_attrset_expression"
) {
return make_container_chunk(node, chunk_name, source, recurse_nix_attrset(expression));
return make_container_chunk(
node,
ChunkKind::Attr,
Some(name),
source,
recurse_nix_attrset(expression),
);
}
make_named_chunk(node, chunk_name, source, recurse_nix_binding_value(node))
make_kind_chunk(node, ChunkKind::Attr, Some(name), source, recurse_nix_binding_value(node))
}
impl LangClassifier for NixHclClassifier {
@@ -77,11 +82,14 @@ impl LangClassifier for NixHclClassifier {
// Nix top-level attrsets should recurse into their binding_set so the file exposes
// structural attr chunks instead of a single opaque attrset_expr leaf.
"attrset_expression" | "let_attrset_expression" | "rec_attrset_expression" => {
Some(make_container_chunk(
Some(make_candidate(
node,
sanitize_node_kind(node.kind()),
source,
ChunkKind::Attrs,
None,
NameStyle::Named,
signature_for_node(node, source),
recurse_nix_attrset(node),
source,
))
},
// Older tree-sitter-nix revisions used `attribute`; current grammars expose `binding`.
@@ -91,29 +99,44 @@ impl LangClassifier for NixHclClassifier {
if let Some(name) = extract_hcl_block_name(node, source) {
Some(make_container_chunk(
node,
format!("block_{name}"),
ChunkKind::Block,
Some(name),
source,
recurse_into(node, ChunkContext::ClassBody, &[], &["body"]),
))
} else {
Some(group_candidate(node, "hunks", source))
Some(group_candidate(node, ChunkKind::Hunks, source))
}
},
// Nix expressions
"function_expression" | "let_expression" => {
Some(named_candidate(node, "expr", source, recurse_value_container(node)))
},
"function_expression" | "let_expression" => Some(named_candidate(
node,
ChunkKind::Expression,
source,
recurse_value_container(node),
)),
// Nix inherit
"inherit" => Some(group_candidate(node, "imports", source)),
"inherit" => Some(group_candidate(node, ChunkKind::Imports, source)),
// Variable/assignment declarations
"variable_declaration" | "assignment" => Some(group_candidate(node, "decls", source)),
"variable_declaration" | "assignment" => {
Some(group_candidate(node, ChunkKind::Declarations, source))
},
// HCL top-level block types
"provider" | "resource" | "data" | "locals" | "variable" | "output" | "module" => {
Some(container_candidate(
let kind = match node.kind() {
"locals" => ChunkKind::BlockLocals,
"variable" => ChunkKind::Variable,
"module" => ChunkKind::Module,
_ => ChunkKind::Block,
};
Some(make_candidate(
node,
sanitize_node_kind(node.kind()).as_str(),
source,
kind,
prefixed_name(sanitize_node_kind(node.kind()).as_str(), node, source),
NameStyle::Named,
signature_for_node(node, source),
recurse_into(node, ChunkContext::ClassBody, &[], &["body"]),
source,
))
},
_ => None,
@@ -126,18 +149,22 @@ impl LangClassifier for NixHclClassifier {
"block" => extract_hcl_block_name(node, source).map(|name| {
make_container_chunk(
node,
format!("block_{name}"),
ChunkKind::Block,
Some(name),
source,
recurse_into(node, ChunkContext::ClassBody, &[], &["body"]),
)
}),
// Nested Nix attrset values recurse into their binding_set just like top-level ones.
"attrset_expression" | "let_attrset_expression" | "rec_attrset_expression" => {
Some(make_container_chunk(
Some(make_candidate(
node,
sanitize_node_kind(node.kind()),
source,
ChunkKind::Attrs,
None,
NameStyle::Named,
signature_for_node(node, source),
recurse_nix_attrset(node),
source,
))
},
// Nested Nix binding
@@ -149,8 +176,8 @@ impl LangClassifier for NixHclClassifier {
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
match node.kind() {
// Nix control flow
"if_expression" => Some(positional_candidate(node, "if", source)),
"let_expression" => Some(positional_candidate(node, "block", source)),
"if_expression" => Some(positional_candidate(node, ChunkKind::If, source)),
"let_expression" => Some(positional_candidate(node, ChunkKind::Block, source)),
_ => None,
}
}
+55 -31
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct OcamlClassifier;
@@ -13,22 +13,24 @@ impl LangClassifier for OcamlClassifier {
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
match node.kind() {
"method_definition" => Some(make_named_chunk(
"method_definition" => Some(make_kind_chunk(
node,
format!("fn_{}", ocaml_named_text(node, source, &["method_name"])?),
ChunkKind::Function,
ocaml_named_text(node, source, &["method_name"]),
source,
ocaml_method_recurse(node),
)),
"method_specification" => Some(make_named_chunk(
"method_specification" => Some(make_kind_chunk(
node,
format!("fn_{}", ocaml_named_text(node, source, &["method_name"])?),
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_named_chunk(node, format!("field_{name}"), source, None),
None => group_candidate(node, "fields", source),
Some(name) => make_kind_chunk(node, ChunkKind::Field, Some(name), source, None),
None => group_candidate(node, ChunkKind::Fields, source),
})
},
_ => classify_ocaml_item(node, source),
@@ -37,23 +39,32 @@ impl LangClassifier for OcamlClassifier {
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
match node.kind() {
"function_expression" | "match_expression" => Some(make_named_chunk(
"function_expression" | "match_expression" => Some(make_candidate(
node,
"match".to_string(),
source,
ChunkKind::Match,
None,
NameStyle::Named,
signature_for_node(node, source),
Some(recurse_self(node, ChunkContext::FunctionBody)),
source,
)),
"match_case" => Some(make_named_chunk(
"match_case" => Some(make_candidate(
node,
"case".to_string(),
source,
ChunkKind::Case,
None,
NameStyle::Named,
signature_for_node(node, source),
Some(recurse_self(node, ChunkContext::FunctionBody)),
source,
)),
"let_expression" => Some(make_named_chunk(
"let_expression" => Some(make_candidate(
node,
"let".to_string(),
source,
ChunkKind::Let,
None,
NameStyle::Named,
signature_for_node(node, source),
Some(recurse_self(node, ChunkContext::FunctionBody)),
source,
)),
_ => classify_ocaml_item(node, source),
}
@@ -62,47 +73,60 @@ impl LangClassifier for OcamlClassifier {
fn classify_ocaml_item<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
Some(match node.kind() {
"open_module" => group_candidate(node, "imports", source),
"open_module" => group_candidate(node, ChunkKind::Imports, source),
"module_definition" => make_container_chunk(
node,
format!("mod_{}", ocaml_named_text(node, source, &["module_name"])?),
ChunkKind::Module,
ocaml_named_text(node, source, &["module_name"]),
source,
ocaml_module_recurse(node),
),
"module_type_definition" => make_container_chunk(
"module_type_definition" => make_candidate(
node,
ChunkKind::Interface,
format!("modtype_{}", ocaml_named_text(node, source, &["module_type_name"])?),
source,
NameStyle::Named,
signature_for_node(node, source),
ocaml_module_type_recurse(node),
source,
),
"class_definition" => make_container_chunk(
node,
format!("class_{}", ocaml_named_text(node, source, &["class_name"])?),
ChunkKind::Class,
ocaml_named_text(node, source, &["class_name"]),
source,
ocaml_class_recurse(node),
),
"class_type_definition" => make_container_chunk(
"class_type_definition" => make_candidate(
node,
ChunkKind::Iface,
format!("classtype_{}", ocaml_named_text(node, source, &["class_type_name"])?),
source,
NameStyle::Named,
signature_for_node(node, source),
ocaml_class_type_recurse(node),
source,
),
"type_definition" => make_named_chunk(
"type_definition" => make_kind_chunk(
node,
format!("type_{}", ocaml_named_text(node, source, &["type_constructor"])?),
ChunkKind::Type,
ocaml_named_text(node, source, &["type_constructor"]),
source,
None,
),
"exception_definition" => make_named_chunk(
"exception_definition" => make_candidate(
node,
ChunkKind::Constructor,
format!("exception_{}", ocaml_named_text(node, source, &["constructor_name"])?),
source,
NameStyle::Named,
signature_for_node(node, source),
None,
source,
),
"value_definition" => classify_ocaml_value_definition(node, source)?,
"value_specification" => make_named_chunk(
"value_specification" => make_kind_chunk(
node,
format!("val_{}", ocaml_named_text(node, source, &["value_name"])?),
ChunkKind::Val,
ocaml_named_text(node, source, &["value_name"]),
source,
None,
),
@@ -117,9 +141,9 @@ fn classify_ocaml_value_definition<'t>(
let name = ocaml_named_text(node, source, &["value_name"])?;
let recurse = ocaml_value_recurse(node);
if ocaml_value_definition_is_function(node) {
Some(make_named_chunk(node, format!("fn_{name}"), source, recurse))
Some(make_kind_chunk(node, ChunkKind::Function, Some(name), source, recurse))
} else {
Some(make_named_chunk(node, format!("val_{name}"), source, recurse))
Some(make_kind_chunk(node, ChunkKind::Val, Some(name), source, recurse))
}
}
+17 -25
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct PerlClassifier;
@@ -25,30 +25,22 @@ fn classify_perl_node<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandid
Some(match node.kind() {
"package_statement" => {
make_named_chunk(node, format!("mod_{}", perl_name(node, source)?), source, None)
make_kind_chunk(node, ChunkKind::Module, Some(perl_name(node, source)?), source, None)
},
"use_statement" => group_candidate(node, "imports", source),
"subroutine_declaration_statement" => {
make_named_chunk(node, format!("fn_{}", perl_name(node, source)?), source, body_recurse())
"use_statement" => group_candidate(node, ChunkKind::Imports, source),
"subroutine_declaration_statement" => make_kind_chunk(
node,
ChunkKind::Function,
Some(perl_name(node, source)?),
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)
},
"conditional_statement" => make_candidate(
node,
"if".to_string(),
NameStyle::Named,
None,
body_recurse(),
false,
source,
),
"for_statement" | "loop_statement" => make_candidate(
node,
"loop".to_string(),
NameStyle::Named,
None,
body_recurse(),
false,
source,
),
"expression_statement" => classify_perl_statement(node, source),
_ => return None,
})
@@ -56,9 +48,9 @@ fn classify_perl_node<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandid
fn classify_perl_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
if perl_declares_variable(node) {
group_candidate(node, "decls", source)
group_candidate(node, ChunkKind::Declarations, source)
} else {
group_candidate(node, "stmts", source)
group_candidate(node, ChunkKind::Statements, source)
}
}
+48 -34
View File
@@ -2,29 +2,32 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct PowershellClassifier;
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, "params", source),
"param_block" => group_candidate(node, ChunkKind::Parameters, source),
"statement_list" => make_container_chunk(
node,
"body".to_string(),
ChunkKind::Body,
None,
source,
Some(recurse_self(node, ChunkContext::Root)),
),
"class_statement" => make_container_chunk(
node,
format!("class_{}", powershell_name(node, source)?),
ChunkKind::Class,
Some(powershell_name(node, source)?),
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
),
"function_statement" => make_named_chunk(
"function_statement" => make_container_chunk(
node,
format!("fn_{}", powershell_name(node, source)?),
ChunkKind::Function,
Some(powershell_name(node, source)?),
source,
Some(recurse_self(node, ChunkContext::FunctionBody)),
),
@@ -32,7 +35,7 @@ impl LangClassifier for PowershellClassifier {
"switch_statement" | "if_statement" | "foreach_statement" => {
return self.classify_function(node, source);
},
"flow_control_statement" => group_candidate(node, "stmts", source),
"flow_control_statement" => group_candidate(node, ChunkKind::Statements, source),
_ => return None,
})
}
@@ -40,8 +43,8 @@ impl LangClassifier for PowershellClassifier {
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_named_chunk(node, format!("field_{name}"), source, None),
None => group_candidate(node, "fields", 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,
@@ -50,51 +53,60 @@ impl LangClassifier for PowershellClassifier {
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, "params", source),
"class_method_parameter_list" | "param_block" => {
group_candidate(node, ChunkKind::Parameters, source)
},
"script_block" => make_container_chunk(
node,
block_name_for_parent(node).to_string(),
block_kind_for_parent(node),
None,
source,
Some(recurse_self(node, ChunkContext::FunctionBody)),
),
"script_block_body" | "statement_block" => make_container_chunk(
node,
"block".to_string(),
ChunkKind::Block,
None,
source,
recurse_into(node, ChunkContext::FunctionBody, &[], &["statement_list"]),
),
"pipeline" => classify_powershell_pipeline(node, source),
"if_statement" => make_named_chunk(
"if_statement" => make_container_chunk(
node,
"if".to_string(),
ChunkKind::If,
None,
source,
recurse_into(node, ChunkContext::FunctionBody, &[], &["statement_block"]),
),
"foreach_statement" => make_named_chunk(
"foreach_statement" => make_container_chunk(
node,
"loop".to_string(),
ChunkKind::Loop,
None,
source,
recurse_into(node, ChunkContext::FunctionBody, &[], &["statement_block"]),
),
"switch_statement" => make_named_chunk(
"switch_statement" => make_container_chunk(
node,
"switch".to_string(),
ChunkKind::Switch,
None,
source,
recurse_into(node, ChunkContext::FunctionBody, &[], &["switch_body"]),
),
"switch_clauses" => make_container_chunk(
node,
"cases".to_string(),
ChunkKind::Cases,
None,
source,
Some(recurse_self(node, ChunkContext::FunctionBody)),
),
"switch_clause" => make_named_chunk(
"switch_clause" => make_container_chunk(
node,
"case".to_string(),
ChunkKind::Case,
None,
source,
recurse_into(node, ChunkContext::FunctionBody, &[], &["statement_block"]),
),
"flow_control_statement" => group_candidate(node, "stmts", source),
"flow_control_statement" => group_candidate(node, ChunkKind::Statements, source),
_ => return None,
})
}
@@ -107,15 +119,16 @@ impl LangClassifier for PowershellClassifier {
fn classify_class_method<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
let name = powershell_name(node, source)?;
let class_name = powershell_name(node.parent()?, source)?;
let chunk_name = if name == "new" || name == class_name {
"constructor".to_string()
let (kind, identifier) = if name == "new" || name == class_name {
(ChunkKind::Constructor, None)
} else {
format!("fn_{name}")
(ChunkKind::Function, Some(name))
};
Some(make_named_chunk(
Some(make_container_chunk(
node,
chunk_name,
kind,
identifier,
source,
Some(recurse_self(node, ChunkContext::FunctionBody)),
))
@@ -125,23 +138,24 @@ fn classify_powershell_pipeline<'t>(node: Node<'t>, source: &str) -> RawChunkCan
if let Some(command_name) = powershell_command_name(node, source)
&& matches!(command_name.as_str(), "using" | "using-module" | "Import-Module")
{
return group_candidate(node, "imports", source);
return group_candidate(node, ChunkKind::Imports, source);
}
if let Some((name, script_block)) = assigned_script_block(node, source) {
return make_container_chunk_from(
node,
node,
format!("block_{name}"),
ChunkKind::Block,
Some(name),
source,
Some(recurse_self(script_block, ChunkContext::FunctionBody)),
);
}
if child_by_kind(node, &["assignment_expression"]).is_some() {
group_candidate(node, "decls", source)
group_candidate(node, ChunkKind::Declarations, source)
} else {
group_candidate(node, "stmts", source)
group_candidate(node, ChunkKind::Statements, source)
}
}
@@ -170,10 +184,10 @@ fn find_script_block(node: Node<'_>) -> Option<Node<'_>> {
None
}
fn block_name_for_parent(node: Node<'_>) -> &'static str {
fn block_kind_for_parent(node: Node<'_>) -> ChunkKind {
match node.parent().map(|parent| parent.kind()) {
Some("function_statement" | "class_method_definition") => "body",
_ => "block",
Some("function_statement" | "class_method_definition") => ChunkKind::Body,
_ => ChunkKind::Block,
}
}
+40 -15
View File
@@ -7,7 +7,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct ProtoClassifier;
@@ -23,23 +23,26 @@ impl LangClassifier for ProtoClassifier {
fn classify_proto_root<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
Some(match node.kind() {
"syntax" | "package" => group_candidate(node, "headers", source),
"import" => group_candidate(node, "imports", source),
"option" => group_candidate(node, "options", source),
"message" => make_container_chunk(
"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,
format!("msg_{}", proto_name(node, source)?),
source,
recurse_into(node, ChunkContext::ClassBody, &[], &["message_body"]),
),
"enum" => make_container_chunk(
node,
format!("enum_{}", proto_name(node, source)?),
ChunkKind::Enum,
proto_name(node, source),
source,
recurse_into(node, ChunkContext::ClassBody, &[], &["enum_body"]),
),
"service" => make_container_chunk(
"service" => make_named_proto_chunk(
node,
ChunkKind::Interface,
format!("service_{}", proto_name(node, source)?),
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
@@ -50,36 +53,40 @@ 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, "options", source),
"option" => group_candidate(node, ChunkKind::Options, source),
"field" if is_proto_message_field(node) => {
make_named_chunk(node, format!("field_{}", proto_name(node, source)?), source, None)
make_kind_chunk(node, ChunkKind::Field, proto_name(node, source), source, None)
},
"oneof" => make_container_chunk(
"oneof" => make_named_proto_chunk(
node,
ChunkKind::Either,
format!("oneof_{}", proto_name(node, source)?),
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
),
"oneof_field" => {
make_named_chunk(node, format!("field_{}", proto_name(node, source)?), source, None)
make_kind_chunk(node, ChunkKind::Field, proto_name(node, source), source, None)
},
"message" => make_container_chunk(
"message" => make_named_proto_chunk(
node,
ChunkKind::Type,
format!("msg_{}", proto_name(node, source)?),
source,
recurse_into(node, ChunkContext::ClassBody, &[], &["message_body"]),
),
"enum" => make_container_chunk(
node,
format!("enum_{}", proto_name(node, source)?),
ChunkKind::Enum,
proto_name(node, source),
source,
recurse_into(node, ChunkContext::ClassBody, &[], &["enum_body"]),
),
"enum_field" => {
make_named_chunk(node, format!("variant_{}", proto_name(node, source)?), source, None)
make_kind_chunk(node, ChunkKind::Variant, proto_name(node, source), source, None)
},
"rpc" => make_container_chunk(
"rpc" => make_named_proto_chunk(
node,
ChunkKind::Proc,
format!("rpc_{}", proto_name(node, source)?),
source,
proto_rpc_recurse(node),
@@ -88,6 +95,24 @@ fn classify_proto_class<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCand
})
}
fn make_named_proto_chunk<'t>(
node: Node<'t>,
kind: ChunkKind,
identifier: impl Into<Option<String>>,
source: &str,
recurse: Option<RecurseSpec<'t>>,
) -> RawChunkCandidate<'t> {
make_candidate(
node,
kind,
identifier,
NameStyle::Named,
signature_for_node(node, source),
recurse,
source,
)
}
fn is_proto_message_field(node: Node<'_>) -> bool {
node
.parent()
+66 -64
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct PythonClassifier;
@@ -11,16 +11,17 @@ impl LangClassifier for PythonClassifier {
match node.kind() {
// ── Imports ──
"import_statement" | "import_from_statement" => {
Some(group_candidate(node, "imports", source))
Some(group_candidate(node, ChunkKind::Imports, source))
},
// ── Variables / assignments ──
"assignment" => Some(group_candidate(node, "decls", source)),
"assignment" => Some(group_candidate(node, ChunkKind::Declarations, source)),
// ── Functions ──
"function_definition" => Some(make_named_chunk(
"function_definition" => Some(make_kind_chunk(
node,
prefixed_name("fn", node, source),
ChunkKind::Function,
extract_identifier(node, source),
source,
recurse_into(node, ChunkContext::FunctionBody, &["body"], &["block"]),
)),
@@ -28,7 +29,8 @@ impl LangClassifier for PythonClassifier {
// ── Containers ──
"class_definition" => Some(make_container_chunk(
node,
prefixed_name("class", node, source),
ChunkKind::Class,
extract_identifier(node, source),
source,
recurse_into(node, ChunkContext::ClassBody, &["body"], &["block"]),
)),
@@ -39,7 +41,7 @@ impl LangClassifier for PythonClassifier {
// ── Statements ──
"expression_statement" | "global_statement" => {
Some(group_candidate(node, "stmts", source))
Some(group_candidate(node, ChunkKind::Statements, source))
},
// ── Decorated ──
@@ -54,14 +56,20 @@ impl LangClassifier for PythonClassifier {
// ── Methods ──
"function_definition" => {
let name = extract_identifier(node, source).unwrap_or_else(|| "anonymous".to_string());
let chunk_name = if name == "__init__" || name == "__new__" {
"constructor".to_string()
let kind = if name == "__init__" || name == "__new__" {
ChunkKind::Constructor
} else {
format!("fn_{name}")
ChunkKind::Function
};
Some(make_named_chunk(
let identifier = if kind == ChunkKind::Constructor {
None
} else {
Some(name)
};
Some(make_kind_chunk(
node,
chunk_name,
kind,
identifier,
source,
recurse_into(node, ChunkContext::FunctionBody, &["body"], &["block"]),
))
@@ -75,14 +83,20 @@ impl LangClassifier for PythonClassifier {
if let Some(child) = inner {
let name =
extract_identifier(child, source).unwrap_or_else(|| "anonymous".to_string());
let chunk_name = if name == "__init__" || name == "__new__" {
"constructor".to_string()
let kind = if name == "__init__" || name == "__new__" {
ChunkKind::Constructor
} else {
format!("fn_{name}")
ChunkKind::Function
};
Some(make_named_chunk(
let identifier = if kind == ChunkKind::Constructor {
None
} else {
Some(name)
};
Some(make_kind_chunk(
node,
chunk_name,
kind,
identifier,
source,
recurse_into(child, ChunkContext::FunctionBody, &["body"], &["block"]),
))
@@ -92,10 +106,12 @@ impl LangClassifier for PythonClassifier {
},
// ── Fields ──
"expression_statement" | "assignment" => Some(group_candidate(node, "fields", source)),
"expression_statement" | "assignment" => {
Some(group_candidate(node, ChunkKind::Fields, source))
},
// ── Type aliases ──
"type_alias_statement" => Some(named_candidate(node, "type", source, None)),
"type_alias_statement" => Some(named_candidate(node, ChunkKind::Type, source, None)),
_ => None,
}
@@ -106,48 +122,50 @@ impl LangClassifier for PythonClassifier {
// ── Control flow ──
"if_statement" => Some(make_candidate(
node,
"if".to_string(),
ChunkKind::If,
None,
NameStyle::Named,
None,
recurse_body(node, ChunkContext::FunctionBody),
false,
source,
)),
"for_statement" | "while_statement" => Some(make_candidate(
node,
"loop".to_string(),
ChunkKind::Loop,
None,
NameStyle::Named,
None,
recurse_body(node, ChunkContext::FunctionBody),
false,
source,
)),
"try_statement" => Some(make_candidate(
node,
"try".to_string(),
ChunkKind::Try,
None,
NameStyle::Named,
None,
recurse_body(node, ChunkContext::FunctionBody),
false,
source,
)),
"with_statement" => Some(make_candidate(
node,
"block".to_string(),
ChunkKind::Block,
None,
NameStyle::Named,
None,
recurse_body(node, ChunkContext::FunctionBody),
false,
source,
)),
// ── Positional ──
"elif_clause" => Some(positional_candidate(node, "elif", source)),
"except_clause" => Some(positional_candidate(node, "except", source)),
"match_statement" => Some(positional_candidate(node, "match", source)),
"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 ──
"expression_statement" | "assignment" => Some(group_candidate(node, "stmts", source)),
"expression_statement" | "assignment" => {
Some(group_candidate(node, ChunkKind::Statements, source))
},
_ => None,
}
@@ -160,36 +178,18 @@ fn classify_function_python<'t>(node: Node<'t>, source: &str) -> RawChunkCandida
let fn_recurse = || recurse_body(node, ChunkContext::FunctionBody);
match node.kind() {
"if_statement" => {
make_candidate(node, "if".to_string(), NameStyle::Named, None, fn_recurse(), false, source)
make_candidate(node, ChunkKind::If, None, NameStyle::Named, None, fn_recurse(), source)
},
"for_statement" | "while_statement" => make_candidate(
node,
"loop".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"try_statement" => make_candidate(
node,
"try".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"with_statement" => make_candidate(
node,
"block".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
_ => group_candidate(node, "stmts", 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),
}
}
@@ -200,16 +200,18 @@ fn classify_decorated<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t>
match inner {
Some(child) if child.kind() == "class_definition" => make_container_chunk(
node,
prefixed_name("class", child, source),
ChunkKind::Class,
extract_identifier(child, source),
source,
recurse_into(child, ChunkContext::ClassBody, &["body"], &["block"]),
),
Some(child) if child.kind() == "function_definition" => make_named_chunk(
Some(child) if child.kind() == "function_definition" => make_kind_chunk(
node,
prefixed_name("fn", child, source),
ChunkKind::Function,
extract_identifier(child, source),
source,
recurse_into(child, ChunkContext::FunctionBody, &["body"], &["block"]),
),
_ => positional_candidate(node, "block", source),
_ => positional_candidate(node, ChunkKind::Block, source),
}
}
+20 -17
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct RClassifier;
@@ -10,21 +10,23 @@ 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, "imports", source),
"call" => group_candidate(node, "stmts", source),
"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, "if", source, recurse_if(node)),
"if_statement" => control_candidate(node, ChunkKind::If, source, recurse_if(node)),
"for_statement" | "while_statement" | "repeat_statement" => {
control_candidate(node, "loop", source, recurse_loop(node))
control_candidate(node, ChunkKind::Loop, source, recurse_loop(node))
},
// ── Bare expressions ──
"identifier" | "subset" | "subset2" | "extract_operator" => {
group_candidate(node, "stmts", source)
group_candidate(node, ChunkKind::Statements, source)
},
_ => return None,
@@ -37,14 +39,14 @@ impl LangClassifier for RClassifier {
"binary_operator" => classify_assignment(node, source, ChunkScope::Function)?,
// ── Control flow ──
"if_statement" => control_candidate(node, "if", source, recurse_if(node)),
"if_statement" => control_candidate(node, ChunkKind::If, source, recurse_if(node)),
"for_statement" | "while_statement" | "repeat_statement" => {
control_candidate(node, "loop", source, recurse_loop(node))
control_candidate(node, ChunkKind::Loop, source, recurse_loop(node))
},
// ── Calls / bare expressions ──
"call" | "identifier" | "subset" | "subset2" | "extract_operator" | "break" | "next"
| "return" => group_candidate(node, "stmts", source),
| "return" => group_candidate(node, ChunkKind::Statements, source),
_ => return None,
})
@@ -66,10 +68,11 @@ fn classify_assignment<'t>(
if rhs.kind() == "function_definition" {
let name = simple_lhs_name(lhs, source).unwrap_or_else(|| "anonymous".to_string());
return Some(make_named_chunk_from(
return Some(make_kind_chunk_from(
node,
rhs,
format!("fn_{name}"),
ChunkKind::Function,
Some(name),
source,
recurse_body(rhs, ChunkContext::FunctionBody),
));
@@ -77,16 +80,16 @@ fn classify_assignment<'t>(
match (scope, simple_lhs_name(lhs, source)) {
(ChunkScope::Root, Some(name)) => {
Some(make_named_chunk(node, format!("var_{name}"), source, None))
Some(make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None))
},
(ChunkScope::Function, Some(name)) if spans_multiple_lines(node) => {
Some(make_named_chunk(node, format!("var_{name}"), source, None))
Some(make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None))
},
_ => Some(group_candidate(
node,
match scope {
ChunkScope::Root => "decls",
ChunkScope::Function => "stmts",
ChunkScope::Root => ChunkKind::Declarations,
ChunkScope::Function => ChunkKind::Statements,
},
source,
)),
@@ -132,11 +135,11 @@ fn recurse_loop(node: Node<'_>) -> Option<RecurseSpec<'_>> {
fn control_candidate<'t>(
node: Node<'t>,
name: &str,
kind: ChunkKind,
source: &str,
recurse: Option<RecurseSpec<'t>>,
) -> RawChunkCandidate<'t> {
make_candidate(node, name.to_string(), NameStyle::Named, None, recurse, true, source)
make_candidate(node, kind, None, NameStyle::Named, None, recurse, source)
}
fn spans_multiple_lines(node: Node<'_>) -> bool {
+40 -36
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct RubyLuaClassifier;
@@ -20,38 +20,46 @@ impl LangClassifier for RubyLuaClassifier {
let target = extract_identifier(node, source);
match target.as_deref() {
Some("require" | "require_relative" | "load" | "autoload") => {
group_candidate(node, "imports", source)
group_candidate(node, ChunkKind::Imports, source)
},
_ => group_candidate(node, "stmts", source),
_ => group_candidate(node, ChunkKind::Statements, source),
}
},
// ── Functions ──
"function_definition" => {
named_candidate(node, "fn", source, recurse_body(node, ChunkContext::FunctionBody))
},
"method" | "singleton_method" => {
named_candidate(node, "fn", source, recurse_body(node, ChunkContext::FunctionBody))
},
"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, "class", source, recurse_class(node)),
"module" => container_candidate(node, "mod", source, recurse_class(node)),
"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, "stmts", source)),
.unwrap_or_else(|| group_candidate(node, ChunkKind::Statements, source)),
);
},
// ── Assignments ──
"assignment" => group_candidate(node, "decls", source),
"assignment" => group_candidate(node, ChunkKind::Declarations, source),
// ── Statements ──
"expression_statement" | "function_call" => group_candidate(node, "stmts", source),
"expression_statement" | "function_call" => {
group_candidate(node, ChunkKind::Statements, source)
},
_ => return None,
})
@@ -63,16 +71,18 @@ impl LangClassifier for RubyLuaClassifier {
"method" | "singleton_method" => {
let name = extract_identifier(node, source).unwrap_or_else(|| "anonymous".to_string());
if name == "initialize" {
make_named_chunk(
make_kind_chunk(
node,
"constructor".to_string(),
ChunkKind::Constructor,
None,
source,
recurse_body(node, ChunkContext::FunctionBody),
)
} else {
make_named_chunk(
make_kind_chunk(
node,
format!("fn_{name}"),
ChunkKind::Function,
Some(name),
source,
recurse_body(node, ChunkContext::FunctionBody),
)
@@ -80,14 +90,14 @@ impl LangClassifier for RubyLuaClassifier {
},
// ── Nested containers ──
"class" => container_candidate(node, "class", source, recurse_class(node)),
"module" => container_candidate(node, "mod", source, recurse_class(node)),
"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, "fields", source),
"assignment" => group_candidate(node, ChunkKind::Fields, source),
// ── Calls (include, attr_reader, etc.) and bare identifiers (private) ──
"call" | "command" | "identifier" => group_candidate(node, "stmts", source),
"call" | "command" | "identifier" => group_candidate(node, ChunkKind::Statements, source),
_ => return None,
})
@@ -97,36 +107,30 @@ impl LangClassifier for RubyLuaClassifier {
let fn_recurse = || recurse_body(node, ChunkContext::FunctionBody);
Some(match node.kind() {
// ── Control flow ──
"if_statement" | "unless" => make_candidate(
node,
"if".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"if_statement" | "unless" => {
make_candidate(node, ChunkKind::If, None, NameStyle::Named, None, fn_recurse(), source)
},
"case_statement" | "case_match" => make_candidate(
node,
"switch".to_string(),
ChunkKind::Switch,
None,
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"while_statement" | "for_statement" => make_candidate(
node,
"loop".to_string(),
ChunkKind::Loop,
None,
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
// ── Variables ──
"assignment" => group_candidate(node, "stmts", source),
"assignment" => group_candidate(node, ChunkKind::Statements, source),
_ => return None,
})
+46 -43
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct RustClassifier;
@@ -10,56 +10,62 @@ 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, "imports", source),
"use_declaration" | "extern_crate_declaration" => {
group_candidate(node, ChunkKind::Imports, source)
},
// ── Functions ──
"function_item" | "function_definition" => named_candidate(
node,
"fn",
ChunkKind::Function,
source,
recurse_body(node, ChunkContext::FunctionBody)
.or_else(|| recurse_into(node, ChunkContext::FunctionBody, &["body"], &["block"])),
),
// ── Containers ──
"struct_item" => container_candidate(node, "struct", source, recurse_class(node)),
"enum_item" => container_candidate(node, "enum", source, recurse_enum(node)),
"trait_item" => container_candidate(node, "trait", source, recurse_class(node)),
"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, "mod", source, recurse_class(node))
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,
format!("impl_{name}"),
ChunkKind::Impl,
Some(name),
source,
recurse_into(node, ChunkContext::ClassBody, &["body"], &["declaration_list"]),
)
},
// ── Types ──
"type_item" => named_candidate(node, "type", source, recurse_class(node)),
"type_item" => named_candidate(node, ChunkKind::Type, source, recurse_class(node)),
// ── Macros ──
"macro_definition" | "macro_rule" => {
named_candidate(node, "macro", source, recurse_body(node, ChunkContext::FunctionBody))
},
"macro_definition" | "macro_rule" => named_candidate(
node,
ChunkKind::Macro,
source,
recurse_body(node, ChunkContext::FunctionBody),
),
// ── Statics / consts ──
"static_item" | "const_item" => group_candidate(node, "decls", source),
"static_item" | "const_item" => group_candidate(node, ChunkKind::Declarations, source),
// ── Attributes ──
"inner_attribute_item" => group_candidate(node, "attrs", source),
"inner_attribute_item" => group_candidate(node, ChunkKind::Attrs, source),
// ── Variables ──
"let_declaration" => match extract_identifier(node, source) {
Some(name) => make_named_chunk(node, format!("var_{name}"), source, None),
None => group_candidate(node, "decls", 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, "stmts", source),
"expression_statement" => group_candidate(node, ChunkKind::Statements, source),
_ => return None,
})
@@ -70,34 +76,35 @@ impl LangClassifier for RustClassifier {
// ── Methods ──
"function_item" | "function_definition" => {
let name = extract_identifier(node, source).unwrap_or_else(|| "anonymous".to_string());
make_named_chunk(
make_kind_chunk(
node,
format!("fn_{name}"),
ChunkKind::Function,
Some(name),
source,
recurse_body(node, ChunkContext::FunctionBody),
)
},
// ── Types ──
"type_item" | "type_alias" => named_candidate(node, "type", source, None),
"type_item" | "type_alias" => named_candidate(node, ChunkKind::Type, source, None),
// ── Fields ──
"field_declaration" => match extract_identifier(node, source) {
Some(name) => make_named_chunk(node, format!("field_{name}"), source, None),
None => group_candidate(node, "fields", 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_named_chunk(node, format!("variant_{name}"), source, None),
None => group_candidate(node, "variants", 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, "fields", source),
"const_item" | "macro_invocation" => group_candidate(node, ChunkKind::Fields, source),
// ── Attributes (absorbed by the framework, but handle explicitly) ──
"attribute_item" => return None, // absorbed by is_absorbable_attr
"attribute_item" => return None,
_ => return None,
})
@@ -107,28 +114,22 @@ impl LangClassifier for RustClassifier {
let fn_recurse = || recurse_body(node, ChunkContext::FunctionBody);
Some(match node.kind() {
// ── Control flow ──
"if_expression" => make_candidate(
node,
"if".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"match_expression" => positional_candidate(node, "match", source),
"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, "loop", source)
positional_candidate(node, ChunkKind::Loop, source)
},
// ── Blocks ──
"unsafe_block" | "async_block" | "const_block" | "block_expression" => make_candidate(
node,
"block".to_string(),
ChunkKind::Block,
None,
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
@@ -137,16 +138,18 @@ impl LangClassifier for RustClassifier {
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_named_chunk(node, format!("var_{name}"), source, None),
None => group_candidate(node, "let", source),
Some(name) => {
make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None)
},
None => group_candidate(node, ChunkKind::Let, source),
}
} else {
group_candidate(node, "let", source)
group_candidate(node, ChunkKind::Let, source)
}
},
// ── Expression statements ──
"expression_statement" => group_candidate(node, "stmts", source),
"expression_statement" => group_candidate(node, ChunkKind::Statements, source),
_ => return None,
})
+74 -41
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct SqlClassifier;
@@ -40,9 +40,10 @@ fn classify_sql_statement_root<'t>(node: Node<'t>, source: &str) -> Option<RawCh
}
if children.iter().any(|child| is_sql_query_kind(child.kind())) {
return Some(make_container_chunk(
return Some(make_named_sql_chunk(
node,
"query".to_string(),
ChunkKind::Query,
None,
source,
Some(recurse_self(node, ChunkContext::FunctionBody)),
));
@@ -57,29 +58,26 @@ fn classify_sql_root_node<'t>(
source: &str,
) -> Option<RawChunkCandidate<'t>> {
Some(match node.kind() {
"create_schema" => make_named_chunk_from(
"create_schema" => make_kind_chunk_from(
range_node,
node,
format!(
"schema_{}",
extract_sql_identifier(node, source).unwrap_or_else(|| "anonymous".to_string())
),
ChunkKind::Schema,
extract_sql_identifier(node, source),
source,
None,
),
"create_table" => make_container_chunk_from(
range_node,
node,
format!(
"table_{}",
extract_sql_object_name(node, source).unwrap_or_else(|| "anonymous".to_string())
),
ChunkKind::Table,
extract_sql_object_name(node, source),
source,
recurse_into(node, ChunkContext::ClassBody, &[], &["column_definitions"]),
),
"create_view" => make_container_chunk_from(
"create_view" => make_named_sql_chunk_from(
range_node,
node,
ChunkKind::Query,
format!(
"view_{}",
extract_sql_object_name(node, source).unwrap_or_else(|| "anonymous".to_string())
@@ -87,9 +85,10 @@ fn classify_sql_root_node<'t>(
source,
recurse_into(node, ChunkContext::FunctionBody, &[], &["create_query"]),
),
"create_materialized_view" => make_container_chunk_from(
"create_materialized_view" => make_named_sql_chunk_from(
range_node,
node,
ChunkKind::Query,
format!(
"matview_{}",
extract_sql_object_name(node, source).unwrap_or_else(|| "anonymous".to_string())
@@ -100,16 +99,15 @@ fn classify_sql_root_node<'t>(
"create_function" => make_container_chunk_from(
range_node,
node,
format!(
"fn_{}",
extract_sql_object_name(node, source).unwrap_or_else(|| "anonymous".to_string())
),
ChunkKind::Function,
extract_sql_object_name(node, source),
source,
recurse_sql_function_query(node),
),
"create_trigger" => make_named_chunk_from(
"create_trigger" => make_named_sql_chunk_from(
range_node,
node,
ChunkKind::Function,
format!(
"trigger_{}",
extract_sql_object_name(node, source).unwrap_or_else(|| "anonymous".to_string())
@@ -117,9 +115,10 @@ fn classify_sql_root_node<'t>(
source,
None,
),
"create_index" => make_named_chunk_from(
"create_index" => make_named_sql_chunk_from(
range_node,
node,
ChunkKind::Key,
format!(
"index_{}",
extract_sql_identifier(node, source).unwrap_or_else(|| "anonymous".to_string())
@@ -133,24 +132,19 @@ fn classify_sql_root_node<'t>(
fn classify_sql_class<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
Some(match node.kind() {
"column_definition" => make_named_chunk(
node,
format!(
"field_{}",
extract_sql_identifier(node, source).unwrap_or_else(|| "anonymous".to_string())
),
source,
None,
),
"column_definition" => {
make_kind_chunk(node, ChunkKind::Field, extract_sql_identifier(node, source), source, None)
},
_ => return None,
})
}
fn classify_sql_function<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
if node.kind() == "statement" {
return Some(make_container_chunk(
return Some(make_named_sql_chunk(
node,
"query".to_string(),
ChunkKind::Query,
None,
source,
Some(recurse_self(node, ChunkContext::FunctionBody)),
));
@@ -161,10 +155,11 @@ fn classify_sql_function<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCan
fn classify_sql_query_node<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
Some(match node.kind() {
"insert" => group_candidate(node, "stmts", source),
"keyword_with" => group_candidate(node, "with", source),
"cte" => make_container_chunk(
"insert" => group_candidate(node, ChunkKind::Statements, source),
"keyword_with" => group_candidate(node, ChunkKind::With, source),
"cte" => make_named_sql_chunk(
node,
ChunkKind::With,
format!(
"cte_{}",
extract_sql_identifier(node, source).unwrap_or_else(|| "anonymous".to_string())
@@ -172,22 +167,60 @@ fn classify_sql_query_node<'t>(node: Node<'t>, source: &str) -> Option<RawChunkC
source,
recurse_into(node, ChunkContext::FunctionBody, &[], &["statement"]),
),
"select" => positional_candidate(node, "select", source),
"from" => make_container_chunk(
"select" => positional_candidate(node, ChunkKind::Select, source),
"from" => make_named_sql_chunk(
node,
ChunkKind::Query,
"from".to_string(),
source,
Some(recurse_self(node, ChunkContext::FunctionBody)),
),
"relation" => group_candidate(node, "relations", source),
"join" => positional_candidate(node, "join", source),
"where" => positional_candidate(node, "where", source),
"group_by" => positional_candidate(node, "group_by", source),
"order_by" => positional_candidate(node, "order_by", source),
"relation" => group_candidate(node, ChunkKind::Relations, source),
"join" => positional_candidate(node, ChunkKind::Join, source),
"where" => positional_candidate(node, ChunkKind::Where, source),
"group_by" => positional_candidate(node, ChunkKind::GroupBy, source),
"order_by" => positional_candidate(node, ChunkKind::OrderBy, source),
_ => return None,
})
}
fn make_named_sql_chunk<'t>(
node: Node<'t>,
kind: ChunkKind,
identifier: impl Into<Option<String>>,
source: &str,
recurse: Option<RecurseSpec<'t>>,
) -> RawChunkCandidate<'t> {
make_candidate(
node,
kind,
identifier,
NameStyle::Named,
signature_for_node(node, source),
recurse,
source,
)
}
fn make_named_sql_chunk_from<'t>(
range_node: Node<'t>,
signature_node: Node<'t>,
kind: ChunkKind,
identifier: impl Into<Option<String>>,
source: &str,
recurse: Option<RecurseSpec<'t>>,
) -> RawChunkCandidate<'t> {
make_candidate(
range_node,
kind,
identifier,
NameStyle::Named,
signature_for_node(signature_node, source),
recurse,
source,
)
}
fn recurse_sql_function_query(node: Node<'_>) -> Option<RecurseSpec<'_>> {
let body = child_by_kind(node, &["function_body"])?;
recurse_into(body, ChunkContext::FunctionBody, &[], &["statement"])
+110 -68
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct SvelteClassifier;
@@ -35,15 +35,15 @@ fn classify_svelte_node<'t>(
"snippet_statement" => Some(classify_snippet_statement(node, source)),
"if_statement" => Some(classify_if_statement(node, source)),
"else_if_statement" => Some(classify_else_if_statement(node, source)),
"else_statement" => Some(make_block_chunk(node, "else", source)),
"else_statement" => Some(classify_else_statement(node, source)),
"each_statement" => Some(classify_each_statement(node, source)),
"await_statement" => Some(classify_await_statement(node, source)),
"then_statement" => Some(classify_then_statement(node, source)),
"catch_statement" => Some(classify_catch_statement(node, source)),
"render_expr" => Some(classify_render_expr(node, source)),
"html_interpolation" => Some(group_candidate(node, "html", source)),
"interpolation" => Some(group_candidate(node, "interpolation", source)),
"expression" => Some(group_candidate(node, "expr", source)),
"html_interpolation" => Some(group_candidate(node, ChunkKind::Html, source)),
"interpolation" => Some(group_candidate(node, ChunkKind::Interpolation, source)),
"expression" => Some(group_candidate(node, ChunkKind::Expression, source)),
"element" if include_plain_elements || element_has_structure(node) => {
classify_element(node, source)
},
@@ -52,103 +52,145 @@ fn classify_svelte_node<'t>(
}
fn classify_script_element<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name = if has_attribute(node, "module", source)
let kind = if has_attribute(node, "module", source)
|| attribute_value(node, "context", source).as_deref() == Some("module")
{
"script_module".to_string()
ChunkKind::ScriptModule
} else {
"script".to_string()
ChunkKind::Script
};
// The grammar exposes script contents as a single `raw_text` child, so the
// element boundary is the most truthful chunk.
make_named_chunk(node, name, source, None)
positional_candidate(node, kind, source)
}
fn classify_style_element<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name = if has_attribute(node, "scoped", source) {
"style_scoped".to_string()
let kind = if has_attribute(node, "scoped", source) {
ChunkKind::StyleScoped
} else {
"style".to_string()
ChunkKind::Style
};
make_named_chunk(node, name, source, None)
positional_candidate(node, kind, source)
}
fn classify_snippet_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name = child_by_kind(node, &["snippet_start_expr"])
let identifier = child_by_kind(node, &["snippet_start_expr"])
.and_then(|start| child_by_kind(start, &["snippet_name"]))
.and_then(|name| sanitize_identifier(node_text(source, name.start_byte(), name.end_byte())))
.map_or_else(|| "snippet".to_string(), |name| format!("snippet_{name}"));
make_block_chunk(node, &name, source)
}
fn classify_if_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name = block_expr_name(node, source, "if_start_expr", &["raw_text_expr"], "if");
make_block_chunk(node, &name, source)
}
fn classify_else_if_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name = block_expr_name(node, source, "else_if_expr", &["raw_text_expr"], "else_if");
make_block_chunk(node, &name, source)
}
fn classify_each_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name = block_expr_name(node, source, "each_start_expr", &["raw_text_each"], "each");
make_block_chunk(node, &name, source)
}
fn classify_await_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name = block_expr_name(node, source, "await_start_expr", &["raw_text_expr"], "await");
make_block_chunk(node, &name, source)
}
fn classify_then_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name = block_expr_name(node, source, "then_expr", &["raw_text_expr"], "then");
make_block_chunk(node, &name, source)
}
fn classify_catch_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name = block_expr_name(node, source, "catch_expr", &["raw_text_expr"], "catch");
make_block_chunk(node, &name, source)
}
fn classify_render_expr<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name = child_by_kind(node, &["snippet_name"])
.and_then(|name| sanitize_identifier(node_text(source, name.start_byte(), name.end_byte())))
.map_or_else(|| "render".to_string(), |name| format!("render_{name}"));
make_named_chunk(node, name, source, None)
}
fn classify_element<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
let tag_name = extract_markup_tag_name(node, source)?;
Some(make_container_chunk(
.and_then(|name| sanitize_identifier(node_text(source, name.start_byte(), name.end_byte())));
force_container(make_container_chunk(
node,
format!("tag_{tag_name}"),
ChunkKind::Snippet,
identifier,
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
))
}
fn make_block_chunk<'t>(node: Node<'t>, name: &str, source: &str) -> RawChunkCandidate<'t> {
make_container_chunk(
fn classify_if_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let identifier = block_expr_identifier(node, source, "if_start_expr", &["raw_text_expr"]);
force_container(make_container_chunk(
node,
name.to_string(),
ChunkKind::If,
identifier,
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
)
))
}
fn block_expr_name(
fn classify_else_if_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let identifier = block_expr_identifier(node, source, "else_if_expr", &["raw_text_expr"])
.map_or_else(|| "if".to_string(), |expr| format!("if_{expr}"));
make_named_container_chunk(node, ChunkKind::Else, identifier, source)
}
fn classify_else_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
force_container(make_container_chunk(
node,
ChunkKind::Else,
None,
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
))
}
fn classify_each_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let expr = block_expr_identifier(node, source, "each_start_expr", &["raw_text_each"]);
let id = expr.map_or_else(|| "each".to_string(), |expr| format!("each_{expr}"));
make_named_container_chunk(node, ChunkKind::Loop, id, source)
}
fn classify_await_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let expr = block_expr_identifier(node, source, "await_start_expr", &["raw_text_expr"]);
let id = expr.map_or_else(|| "await".to_string(), |expr| format!("await_{expr}"));
make_named_container_chunk(node, ChunkKind::With, id, source)
}
fn classify_then_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let expr = block_expr_identifier(node, source, "then_expr", &["raw_text_expr"]);
let id = expr.map_or_else(|| "then".to_string(), |expr| format!("then_{expr}"));
make_named_container_chunk(node, ChunkKind::After, id, source)
}
fn classify_catch_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let identifier = block_expr_identifier(node, source, "catch_expr", &["raw_text_expr"]);
force_container(make_container_chunk(
node,
ChunkKind::Catch,
identifier,
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
))
}
fn classify_render_expr<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let identifier = child_by_kind(node, &["snippet_name"])
.and_then(|name| sanitize_identifier(node_text(source, name.start_byte(), name.end_byte())));
make_kind_chunk(node, ChunkKind::Render, identifier, source, None)
}
fn classify_element<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
let tag_name = extract_markup_tag_name(node, source)?;
Some(force_container(make_container_chunk(
node,
ChunkKind::Tag,
Some(tag_name),
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
)))
}
fn make_named_container_chunk<'t>(
node: Node<'t>,
kind: ChunkKind,
identifier: impl Into<Option<String>>,
source: &str,
) -> RawChunkCandidate<'t> {
force_container(make_candidate(
node,
kind,
identifier,
NameStyle::Named,
signature_for_node(node, source),
Some(recurse_self(node, ChunkContext::ClassBody)),
source,
))
}
const fn force_container(mut candidate: RawChunkCandidate<'_>) -> RawChunkCandidate<'_> {
candidate.force_recurse = true;
candidate
}
fn block_expr_identifier(
node: Node<'_>,
source: &str,
header_kind: &str,
expr_kinds: &[&str],
prefix: &str,
) -> String {
) -> Option<String> {
child_by_kind(node, &[header_kind])
.and_then(|header| child_by_kind(header, expr_kinds))
.and_then(|expr| sanitize_identifier(node_text(source, expr.start_byte(), expr.end_byte())))
.map_or_else(|| prefix.to_string(), |expr| format!("{prefix}_{expr}"))
}
fn element_has_structure(node: Node<'_>) -> bool {
+39 -35
View File
@@ -11,9 +11,10 @@ use super::{
classify::LangClassifier,
common::{
ChunkContext, RawChunkCandidate, RecurseSpec, child_by_kind, extract_identifier,
group_candidate, make_container_chunk, make_container_chunk_from, make_named_chunk,
group_candidate, make_container_chunk, make_container_chunk_from, make_kind_chunk,
positional_candidate, recurse_self, sanitize_identifier,
},
kind::ChunkKind,
types::ChunkNode,
};
@@ -24,25 +25,32 @@ impl LangClassifier for TlaplusClassifier {
match node.kind() {
"module" => Some(make_container_chunk(
node,
prefixed(node, "mod", source),
ChunkKind::Module,
tla_identifier(node, source),
source,
Some(recurse_self(node, ChunkContext::Root)),
)),
"variable_declaration" | "constant_declaration" | "recursive_declaration" => {
Some(group_candidate(node, "decls", source))
},
"operator_definition" => {
Some(make_named_chunk(node, prefixed(node, "operator", source), source, None))
Some(group_candidate(node, ChunkKind::Declarations, source))
},
"operator_definition" => Some(make_kind_chunk(
node,
ChunkKind::Operator,
tla_identifier(node, source),
source,
None,
)),
"module_definition" => Some(make_container_chunk(
node,
prefixed(node, "mod", source),
ChunkKind::Module,
tla_identifier(node, source),
source,
Some(recurse_self(node, ChunkContext::Root)),
)),
"pcal_algorithm" => Some(make_container_chunk(
node,
prefixed(node, "algo", source),
ChunkKind::Algo,
tla_identifier(node, source),
source,
recurse_child(node, "pcal_algorithm_body", ChunkContext::ClassBody),
)),
@@ -50,7 +58,8 @@ impl LangClassifier for TlaplusClassifier {
make_container_chunk_from(
node,
algorithm,
prefixed(algorithm, "algo", source),
ChunkKind::Algo,
tla_identifier(algorithm, source),
source,
recurse_child(algorithm, "pcal_algorithm_body", ChunkContext::ClassBody),
)
@@ -63,17 +72,19 @@ impl LangClassifier for TlaplusClassifier {
match node.kind() {
"pcal_procedure" => Some(make_container_chunk(
node,
prefixed(node, "proc", source),
ChunkKind::Proc,
tla_identifier(node, source),
source,
recurse_child(node, "pcal_algorithm_body", ChunkContext::ClassBody),
)),
"pcal_process" => Some(make_container_chunk(
node,
prefixed(node, "process", source),
ChunkKind::Process,
tla_identifier(node, source),
source,
recurse_child(node, "pcal_algorithm_body", ChunkContext::ClassBody),
)),
"pcal_var_decls" => Some(group_candidate(node, "decls", source)),
"pcal_var_decls" => Some(group_candidate(node, ChunkKind::Declarations, source)),
_ => None,
}
}
@@ -81,12 +92,12 @@ impl LangClassifier for TlaplusClassifier {
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, "if", source)),
"pcal_while" => Some(positional_candidate(node, "loop", source)),
"pcal_either" => Some(positional_candidate(node, "either", source)),
"pcal_with" => Some(positional_candidate(node, "with", source)),
"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, "stmts", source)),
"pcal_assign" => Some(group_candidate(node, ChunkKind::Statements, source)),
_ => None,
}
}
@@ -108,11 +119,10 @@ impl LangClassifier for TlaplusClassifier {
parent: &RawChunkCandidate<'_>,
_children: &[RawChunkCandidate<'_>],
) -> bool {
let name = parent.base_name.as_str();
name.starts_with("mod_")
|| name.starts_with("algo_")
|| name.starts_with("proc_")
|| name.starts_with("process_")
matches!(
parent.kind,
ChunkKind::Module | ChunkKind::Algo | ChunkKind::Proc | ChunkKind::Process
)
}
fn post_process(
@@ -174,14 +184,11 @@ impl LangClassifier for TlaplusClassifier {
}
}
fn prefixed(node: Node<'_>, prefix: &str, source: &str) -> String {
let name = extract_identifier(node, source)
.or_else(|| {
child_by_kind(node, &["identifier"])
.and_then(|child| sanitize_identifier(child.utf8_text(source.as_bytes()).ok()?))
})
.unwrap_or_else(|| "anonymous".to_string());
format!("{prefix}_{name}")
fn tla_identifier(node: Node<'_>, source: &str) -> Option<String> {
extract_identifier(node, source).or_else(|| {
child_by_kind(node, &["identifier"])
.and_then(|child| sanitize_identifier(child.utf8_text(source.as_bytes()).ok()?))
})
}
fn recurse_child<'tree>(
@@ -255,7 +262,8 @@ fn translation_chunk(
let checksum = super::chunk_checksum(&source.as_bytes()[start_byte as usize..end_byte as usize]);
ChunkNode {
path,
name: format!("translation_{}", range.start_line),
identifier: Some(range.start_line.to_string()),
kind: ChunkKind::Translation,
leaf: true,
parent_path,
children: Vec::new(),
@@ -299,10 +307,6 @@ fn byte_range_for_lines(source: &str, start_line: u32, end_line: u32) -> (u32, u
}
if byte == b'\n' {
current_line += 1;
if current_line > end_line {
end_byte = byte_index + 1;
break;
}
}
}
+92 -39
View File
@@ -2,7 +2,7 @@
use tree_sitter::Node;
use super::{classify::LangClassifier, common::*};
use super::{classify::LangClassifier, common::*, kind::ChunkKind};
pub struct VueClassifier;
@@ -43,59 +43,58 @@ fn classify_nested_node<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCand
"start_tag" => classify_start_tag(node, source),
"directive_attribute" => Some(classify_directive_attribute(node, source)),
"attribute" => Some(classify_attribute(node, source)),
"interpolation" => Some(make_named_chunk(node, "expr".to_string(), source, None)),
"text" => Some(group_candidate(node, "text", source)),
"raw_text" => Some(group_candidate(node, "text", source)),
"interpolation" => Some(make_kind_chunk(node, ChunkKind::Expression, None, source, None)),
"text" => Some(group_candidate(node, ChunkKind::Text, source)),
"raw_text" => Some(group_candidate(node, ChunkKind::Text, source)),
_ => None,
}
}
fn classify_template_element<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name = extract_slot_name(node, source)
.map_or_else(|| "template".to_string(), |slot_name| format!("slot_{slot_name}"));
make_container_chunk(node, name, source, Some(recurse_self(node, ChunkContext::ClassBody)))
let recurse = Some(recurse_self(node, ChunkContext::ClassBody));
if let Some(slot_name) = extract_slot_name(node, source) {
force_container(make_container_chunk(node, ChunkKind::Slot, Some(slot_name), source, recurse))
} else {
force_container(make_container_chunk(node, ChunkKind::Template, None, source, recurse))
}
}
fn classify_script_element<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name = if has_attribute(node, "setup", source) {
"script_setup".to_string()
let kind = if has_attribute(node, "setup", source) {
ChunkKind::ScriptSetup
} else if attribute_value(node, "context", source).as_deref() == Some("module") {
"script_module".to_string()
ChunkKind::ScriptModule
} else {
"script".to_string()
ChunkKind::Script
};
// tree-sitter-vue exposes script bodies as `raw_text`, not injected JS/TS.
make_named_chunk(node, name, source, None)
positional_candidate(node, kind, source)
}
fn classify_style_element<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name = if has_attribute(node, "scoped", source) {
"style_scoped".to_string()
let kind = if has_attribute(node, "scoped", source) {
ChunkKind::StyleScoped
} else {
"style".to_string()
ChunkKind::Style
};
// Styles are likewise exposed as `raw_text`, so preserve only the SFC block.
make_named_chunk(node, name, source, None)
positional_candidate(node, kind, source)
}
fn classify_custom_block<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let tag_name = extract_markup_tag_name(node, source).unwrap_or_else(|| "anonymous".to_string());
make_container_chunk(
node,
format!("custom_{tag_name}"),
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
)
make_named_container_chunk(node, ChunkKind::Custom, tag_name, source)
}
fn classify_element<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
let tag_name = extract_markup_tag_name(node, source)?;
Some(make_container_chunk(
Some(force_container(make_container_chunk(
node,
format!("tag_{tag_name}"),
ChunkKind::Tag,
Some(tag_name),
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
))
)))
}
fn classify_start_tag<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
@@ -108,19 +107,14 @@ fn classify_start_tag<'t>(node: Node<'t>, source: &str) -> Option<RawChunkCandid
let tag_name = child_by_kind(node, &["tag_name"])
.and_then(|tag| sanitize_identifier(node_text(source, tag.start_byte(), tag.end_byte())))
.unwrap_or_else(|| "anonymous".to_string());
Some(make_container_chunk(
node,
format!("attrs_{tag_name}"),
source,
Some(recurse_self(node, ChunkContext::ClassBody)),
))
Some(make_named_container_chunk(node, ChunkKind::Attrs, tag_name, source))
}
fn classify_attribute<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
let name = child_by_kind(node, &["attribute_name"])
.and_then(|name| sanitize_identifier(node_text(source, name.start_byte(), name.end_byte())))
.unwrap_or_else(|| "attr".to_string());
make_named_chunk(node, format!("attr_{name}"), source, None)
make_kind_chunk(node, ChunkKind::Attr, Some(name), source, None)
}
fn classify_directive_attribute<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
@@ -131,16 +125,75 @@ fn classify_directive_attribute<'t>(node: Node<'t>, source: &str) -> RawChunkCan
.filter(|mods| !mods.is_empty())
.map(|mods| format!("_{mods}"))
.unwrap_or_default();
let chunk_name = if raw.starts_with('@') {
format!("on_{directive_name}{modifier_suffix}")
if raw.starts_with('@') {
make_named_leaf_chunk(
node,
ChunkKind::Directive,
format!("on_{directive_name}{modifier_suffix}"),
source,
)
} else if raw.starts_with(':') {
format!("bind_{directive_name}{modifier_suffix}")
make_named_leaf_chunk(
node,
ChunkKind::Directive,
format!("bind_{directive_name}{modifier_suffix}"),
source,
)
} else if raw.starts_with('#') {
format!("slot_{directive_name}{modifier_suffix}")
make_kind_chunk(
node,
ChunkKind::Slot,
Some(format!("{directive_name}{modifier_suffix}")),
source,
None,
)
} else {
format!("dir_{directive_name}{modifier_suffix}")
};
make_named_chunk(node, chunk_name, source, None)
make_named_leaf_chunk(
node,
ChunkKind::Directive,
format!("dir_{directive_name}{modifier_suffix}"),
source,
)
}
}
fn make_named_leaf_chunk<'t>(
node: Node<'t>,
kind: ChunkKind,
identifier: impl Into<Option<String>>,
source: &str,
) -> RawChunkCandidate<'t> {
make_candidate(
node,
kind,
identifier,
NameStyle::Named,
signature_for_node(node, source),
None,
source,
)
}
fn make_named_container_chunk<'t>(
node: Node<'t>,
kind: ChunkKind,
identifier: impl Into<Option<String>>,
source: &str,
) -> RawChunkCandidate<'t> {
force_container(make_candidate(
node,
kind,
identifier,
NameStyle::Named,
signature_for_node(node, source),
Some(recurse_self(node, ChunkContext::ClassBody)),
source,
))
}
const fn force_container(mut candidate: RawChunkCandidate<'_>) -> RawChunkCandidate<'_> {
candidate.force_recurse = true;
candidate
}
fn extract_markup_tag_name(node: Node<'_>, source: &str) -> Option<String> {
+77 -117
View File
@@ -6,7 +6,10 @@
use tree_sitter::Node;
use super::types::ChunkNode;
use super::{
kind::{ChunkKind, SummaryStyle},
types::ChunkNode,
};
use crate::env_uint;
// ── Configuration (environment overrides) ────────────────────────────────
@@ -19,6 +22,12 @@ env_uint! {
pub static MIN_RECURSE_SAVINGS: usize = "PI_CHUNK_MIN_SAVINGS" or 4 => [1, usize::MAX];
}
/// Always recurse into named (non-group) chunks when children exist,
/// regardless of the leaf threshold. Disabled via `PI_CHUNK_ALWAYS_RECURSE=0`.
pub static ALWAYS_RECURSE: std::sync::LazyLock<bool> = std::sync::LazyLock::new(|| {
std::env::var("PI_CHUNK_ALWAYS_RECURSE").map_or(true, |v| !matches!(v.as_str(), "0" | "false"))
});
// ── Internal types ───────────────────────────────────────────────────────
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -43,7 +52,8 @@ pub struct RecurseSpec<'tree> {
#[derive(Clone, Debug)]
pub struct RawChunkCandidate<'tree> {
pub base_name: String,
pub identifier: Option<String>,
pub kind: ChunkKind,
pub name_style: NameStyle,
pub range_start_byte: usize,
pub range_end_byte: usize,
@@ -88,20 +98,21 @@ const fn end_row_as_line(start: tree_sitter::Point, end: tree_sitter::Point) ->
pub fn make_candidate<'tree>(
node: Node<'tree>,
base_name: String,
kind: ChunkKind,
identifier: impl Into<Option<String>>,
name_style: NameStyle,
signature: Option<String>,
recurse: Option<RecurseSpec<'tree>>,
force_recurse: bool,
source: &str,
) -> RawChunkCandidate<'tree> {
let identifier = identifier.into();
let start = node.start_position();
let end = node.end_position();
let base_name = canonical_chunk_name(node, base_name.as_str(), source);
let summary = summary_for_node(node, base_name.as_str(), signature.as_deref(), source);
let summary = summary_for_node(node, kind, identifier.as_deref(), signature.as_deref(), source);
let start_byte = node.start_byte();
RawChunkCandidate {
base_name,
identifier,
kind,
name_style,
range_start_byte: start_byte,
range_end_byte: node.end_byte(),
@@ -109,96 +120,99 @@ pub fn make_candidate<'tree>(
range_start_line: start.row + 1,
range_end_line: end_row_as_line(start, end),
signature: summary,
error: name_style == NameStyle::Error,
groupable: matches!(name_style, NameStyle::Group),
error: kind == ChunkKind::Error,
groupable: kind.traits().groupable,
has_leading_comment: false,
force_recurse,
force_recurse: kind.traits().container,
recurse,
}
}
pub fn group_candidate<'tree>(
node: Node<'tree>,
base_name: &str,
kind: ChunkKind,
source: &str,
) -> RawChunkCandidate<'tree> {
make_candidate(node, base_name.to_string(), NameStyle::Group, None, None, false, source)
make_candidate(node, kind, None, NameStyle::Group, None, None, source)
}
pub fn positional_candidate<'tree>(
node: Node<'tree>,
base_name: &str,
kind: ChunkKind,
source: &str,
) -> RawChunkCandidate<'tree> {
make_candidate(node, base_name.to_string(), NameStyle::Named, None, None, false, source)
make_candidate(node, kind, None, NameStyle::Named, None, None, source)
}
pub fn named_candidate<'tree>(
node: Node<'tree>,
prefix: &str,
kind: ChunkKind,
source: &str,
recurse: Option<RecurseSpec<'tree>>,
) -> RawChunkCandidate<'tree> {
make_named_chunk(node, prefixed_name(prefix, node, source), source, recurse)
make_kind_chunk(node, kind, extract_identifier(node, source), source, recurse)
}
pub fn container_candidate<'tree>(
node: Node<'tree>,
prefix: &str,
kind: ChunkKind,
source: &str,
recurse: Option<RecurseSpec<'tree>>,
) -> RawChunkCandidate<'tree> {
make_container_chunk(node, prefixed_name(prefix, node, source), source, recurse)
make_kind_chunk(node, kind, extract_identifier(node, source), source, recurse)
}
pub fn make_named_chunk<'tree>(
pub fn make_kind_chunk<'tree>(
node: Node<'tree>,
name: String,
kind: ChunkKind,
identifier: Option<String>,
source: &str,
recurse: Option<RecurseSpec<'tree>>,
) -> RawChunkCandidate<'tree> {
make_candidate(
node,
name,
kind,
identifier,
NameStyle::Named,
signature_for_node(node, source),
recurse,
false,
source,
)
}
pub fn make_named_chunk_from<'tree>(
pub fn make_kind_chunk_from<'tree>(
range_node: Node<'tree>,
signature_node: Node<'tree>,
name: String,
kind: ChunkKind,
identifier: Option<String>,
source: &str,
recurse: Option<RecurseSpec<'tree>>,
) -> RawChunkCandidate<'tree> {
make_candidate(
range_node,
name,
kind,
identifier,
NameStyle::Named,
signature_for_node(signature_node, source),
recurse,
false,
source,
)
}
pub fn make_container_chunk<'tree>(
node: Node<'tree>,
name: String,
kind: ChunkKind,
identifier: Option<String>,
source: &str,
recurse: Option<RecurseSpec<'tree>>,
) -> RawChunkCandidate<'tree> {
make_candidate(
node,
name,
kind,
identifier,
NameStyle::Named,
signature_for_node(node, source),
recurse,
true,
source,
)
}
@@ -206,17 +220,18 @@ pub fn make_container_chunk<'tree>(
pub fn make_container_chunk_from<'tree>(
range_node: Node<'tree>,
signature_node: Node<'tree>,
name: String,
kind: ChunkKind,
identifier: Option<String>,
source: &str,
recurse: Option<RecurseSpec<'tree>>,
) -> RawChunkCandidate<'tree> {
make_candidate(
range_node,
name,
kind,
identifier,
NameStyle::Named,
signature_for_node(signature_node, source),
recurse,
true,
source,
)
}
@@ -231,12 +246,9 @@ pub fn prefixed_name(prefix: &str, node: Node<'_>, source: &str) -> String {
/// Derive a semantic name from a node's kind and/or identifier.
pub fn infer_named_candidate<'tree>(node: Node<'tree>, source: &str) -> RawChunkCandidate<'tree> {
let kind_prefix = sanitize_node_kind(node.kind());
let name = match extract_identifier(node, source) {
Some(id) => format!("{kind_prefix}_{id}"),
None => kind_prefix,
};
make_named_chunk(node, name, source, None)
let kind_name = sanitize_node_kind(node.kind());
let kind = ChunkKind::from_sanitized_kind(kind_name.as_str());
make_kind_chunk(node, kind, extract_identifier(node, source), source, None)
}
// ── Tree navigation helpers ──────────────────────────────────────────────
@@ -634,67 +646,6 @@ pub fn signature_for_node(node: Node<'_>, source: &str) -> Option<String> {
// ── Summary / canonical naming ───────────────────────────────────────────
fn is_function_like_kind(kind: &str) -> bool {
matches!(
kind,
"function_declaration"
| "function_definition"
| "function_item"
| "procedure_declaration"
| "overloaded_procedure_declaration"
| "function_definition_header"
| "test_declaration"
| "method_definition"
| "method_signature"
| "abstract_method_signature"
| "method_declaration"
| "protocol_function_declaration"
| "method"
| "singleton_method"
)
}
fn is_variable_decl_kind(kind: &str) -> bool {
matches!(
kind,
"lexical_declaration"
| "variable_declaration"
| "const_declaration"
| "var_declaration"
| "let_declaration"
| "short_var_declaration"
)
}
fn canonical_chunk_name(node: Node<'_>, base_name: &str, source: &str) -> String {
if is_function_like_kind(node.kind())
&& !base_name.starts_with("fn_")
&& base_name != "constructor"
&& let Some(name) = extract_identifier(node, source)
{
return format!("fn_{name}");
}
if is_variable_decl_kind(node.kind())
&& !base_name.starts_with("var_")
&& !base_name.starts_with("fn_")
&& !base_name.starts_with("class_")
&& let Some(name) = extract_single_declarator_name(node, source)
{
return format!("var_{name}");
}
if (base_name == "expression" || base_name == "expr")
&& let Some(name) = extract_identifier(node, source)
{
return format!("expr_{name}");
}
if base_name == "return"
&& let Some(name) = extract_identifier(node, source)
{
return format!("ret_{name}");
}
base_name.to_string()
}
fn normalize_summary_text(summary: &str) -> Option<String> {
let summary = collapse_whitespace(summary.trim())
.trim_end_matches('{')
@@ -709,22 +660,29 @@ fn normalize_summary_text(summary: &str) -> Option<String> {
}
}
fn summarize_function_node(canonical_name: &str, raw_signature: &str) -> String {
let name = canonical_name.strip_prefix("fn_").unwrap_or(canonical_name);
fn summarize_function_node(
kind: ChunkKind,
identifier: Option<&str>,
raw_signature: &str,
) -> String {
let name = identifier.unwrap_or_else(|| kind.prefix());
let tail = function_signature(raw_signature)
.or_else(|| python_function_signature(raw_signature))
.or_else(|| rust_function_signature(raw_signature))
.unwrap_or_else(|| raw_signature.to_string());
let tail = tail.replacen("): ", ") → ", 1);
format!("fn {name}{tail}")
format!("{} {name}{tail}", kind.prefix())
}
fn summarize_variable_node(node: Node<'_>, canonical_name: &str, source: &str) -> Option<String> {
fn summarize_variable_node(
node: Node<'_>,
kind: ChunkKind,
identifier: Option<&str>,
source: &str,
) -> Option<String> {
let header = normalized_header(source, node.start_byte(), node.end_byte());
let keyword = header.split_whitespace().next()?;
let name = canonical_name
.strip_prefix("var_")
.unwrap_or(canonical_name);
let name = identifier.unwrap_or_else(|| kind.prefix());
Some(format!("{keyword} {name}"))
}
@@ -734,20 +692,22 @@ fn summarize_statement_node(node: Node<'_>, source: &str) -> Option<String> {
pub fn summary_for_node(
node: Node<'_>,
canonical_name: &str,
kind: ChunkKind,
identifier: Option<&str>,
raw_signature: Option<&str>,
source: &str,
) -> Option<String> {
if canonical_name == "imports" {
return Some("imports".to_string());
}
if canonical_name.starts_with("fn_")
&& let Some(signature) = raw_signature
{
return Some(summarize_function_node(canonical_name, signature));
}
if canonical_name.starts_with("var_") {
return summarize_variable_node(node, canonical_name, source);
match kind.traits().summary {
SummaryStyle::Imports => return Some("imports".to_string()),
SummaryStyle::Function => {
if let Some(signature) = raw_signature {
return Some(summarize_function_node(kind, identifier, signature));
}
},
SummaryStyle::Variable => {
return summarize_variable_node(node, kind, identifier, source);
},
SummaryStyle::Default => {},
}
if matches!(
node.kind(),
+14 -14
View File
@@ -6,40 +6,32 @@
use tree_sitter::Node;
use super::common::*;
// ── Root-level default ──────────────────────────────────────────────────
use super::{common::*, kind::ChunkKind};
pub fn classify_root_default<'tree>(node: Node<'tree>, source: &str) -> RawChunkCandidate<'tree> {
infer_named_candidate(node, source)
}
// ── Class-level default ─────────────────────────────────────────────────
pub fn classify_class_default<'tree>(node: Node<'tree>, source: &str) -> RawChunkCandidate<'tree> {
infer_named_candidate(node, source)
}
// ── Function-level default ──────────────────────────────────────────────
pub fn classify_function_default<'tree>(
node: Node<'tree>,
source: &str,
) -> RawChunkCandidate<'tree> {
let kind_name = sanitize_node_kind(node.kind());
group_candidate(node, &kind_name, source)
group_candidate(node, ChunkKind::from_sanitized_kind(kind_name.as_str()), source)
}
// ── Variable declaration classification (shared) ────────────────────────
pub fn classify_var_decl<'tree>(node: Node<'tree>, source: &str) -> RawChunkCandidate<'tree> {
if let Some(candidate) = promote_assigned_expression(node, node, source) {
return candidate;
}
if let Some(name) = extract_single_declarator_name(node, source) {
return make_named_chunk(node, format!("var_{name}"), source, None);
return make_kind_chunk(node, ChunkKind::Variable, Some(name), source, None);
}
group_candidate(node, "decls", source)
group_candidate(node, ChunkKind::Declarations, source)
}
pub fn promote_assigned_expression<'tree>(
@@ -62,14 +54,22 @@ pub fn promote_assigned_expression<'tree>(
match value.kind() {
"arrow_function" | "function_expression" | "function" => {
let recurse = recurse_body(value, ChunkContext::FunctionBody);
Some(make_named_chunk_from(range_node, value, format!("fn_{name}"), source, recurse))
Some(make_kind_chunk_from(
range_node,
value,
ChunkKind::Function,
Some(name),
source,
recurse,
))
},
"class" | "class_expression" => {
let recurse = recurse_class(value);
Some(make_container_chunk_from(
range_node,
value,
format!("class_{name}"),
ChunkKind::Class,
Some(name),
source,
recurse,
))
+93 -44
View File
@@ -5,9 +5,10 @@ use crate::chunk::{
denormalize_from_tabs, detect_file_indent_char, detect_file_indent_step,
normalize_leading_whitespace_char, reindent_inserted_block, strip_content_prefixes,
},
kind::ChunkKind,
resolve::{
ParsedSelector, chunk_region_range, chunk_supports_region, resolve_chunk_selector,
resolve_chunk_with_crc, sanitize_chunk_selector, sanitize_crc, split_selector_crc_and_region,
ParsedSelector, chunk_region_range, resolve_chunk_selector, resolve_chunk_with_crc,
sanitize_chunk_selector, sanitize_crc, split_selector_crc_and_region,
},
state::{ChunkState, ChunkStateInner},
types::{
@@ -309,11 +310,6 @@ fn resolve_edit_target(
validate_batch_crc(resolved.chunk, resolved.crc.as_deref(), requires_checksum)?;
}
let chunk = resolved.chunk.clone();
if let Some(r) = region
&& !chunk_supports_region(&chunk, r)
{
return Err(format!("Chunk \"{}\" does not support @{}.", chunk.path, r.as_str()));
}
Ok(ResolvedEditTarget { chunk, region })
}
@@ -341,7 +337,7 @@ fn apply_replace(
let anchor = target.chunk;
let (region_start, region_end) = match target.region {
None => (anchor.start_byte as usize, anchor.end_byte as usize),
Some(r) => chunk_region_range(&anchor, r)?,
Some(r) => chunk_region_range(&anchor, r),
};
// Scoped find/replace: locate a literal substring inside the chunk and replace
@@ -461,7 +457,7 @@ fn apply_delete(
let anchor = target.chunk;
if let Some(r) = target.region {
let (range_start, range_end) = chunk_region_range(&anchor, r)?;
let (range_start, range_end) = chunk_region_range(&anchor, r);
state.source = replace_byte_range(&state.source, range_start, range_end, "");
} else {
let offsets = line_offsets(&state.source);
@@ -934,11 +930,12 @@ fn chunk_slice(text: &str, chunk: &ChunkNode) -> String {
.join("\n")
}
fn is_container_like_chunk(chunk: &ChunkNode) -> bool {
const fn is_container_like_chunk(chunk: &ChunkNode) -> bool {
let traits = chunk.kind.traits();
!chunk.leaf
|| ["class_", "type_", "interface_", "enum_", "struct_", "impl_", "trait_", "mod_"]
.iter()
.any(|prefix| chunk.name.starts_with(prefix))
|| traits.container
|| traits.has_addressable_members
|| traits.always_preserve_children
}
fn go_receiver_belongs_to_type(source: &str, chunk: &ChunkNode, type_name: &str) -> bool {
@@ -952,11 +949,14 @@ fn go_receiver_belongs_to_type(source: &str, chunk: &ChunkNode, type_name: &str)
}
fn owned_container_end_line(state: &ChunkStateInner, anchor: &ChunkNode) -> u32 {
if state.language != "go" || !anchor.path.starts_with("type_") {
if state.language != "go" || anchor.kind != ChunkKind::Type {
return anchor.end_line;
}
let type_name = anchor.path.trim_start_matches("type_");
let type_name = anchor
.identifier
.as_deref()
.unwrap_or_else(|| anchor.kind.prefix());
let mut owned_end_line = anchor.end_line;
let mut top_level_chunks = state
.tree
@@ -977,7 +977,7 @@ fn owned_container_end_line(state: &ChunkStateInner, anchor: &ChunkNode) -> u32
if chunk.start_line < owned_end_line {
continue;
}
if chunk.name.starts_with("fn_")
if chunk.kind == ChunkKind::Function
&& go_receiver_belongs_to_type(&state.source, chunk, type_name)
{
owned_end_line = chunk.end_line;
@@ -1018,7 +1018,7 @@ fn body_insertion_point(
at_end: bool,
file_indent_char: char,
file_indent_step: usize,
) -> Result<InsertionPoint, String> {
) -> InsertionPoint {
let offsets = line_offsets(&state.source);
let indent = compute_insert_indent(state, anchor, true, file_indent_char, file_indent_step);
if at_end {
@@ -1034,13 +1034,13 @@ fn body_insertion_point(
} else {
last_child.indent_char.repeat(last_child.indent as usize)
};
return Ok(InsertionPoint {
return InsertionPoint {
offset: line_end_offset(&offsets, last_child.end_line, &state.source),
indent: child_indent,
});
};
}
let (_, body_end) = chunk_region_range(anchor, ChunkRegion::Inner)?;
return Ok(InsertionPoint { offset: body_end, indent });
let (_, body_end) = chunk_region_range(anchor, ChunkRegion::Inner);
return InsertionPoint { offset: body_end, indent };
}
if let Some(first_child_path) = anchor.children.first()
@@ -1050,13 +1050,13 @@ fn body_insertion_point(
.iter()
.find(|chunk| &chunk.path == first_child_path)
{
return Ok(InsertionPoint {
return InsertionPoint {
offset: line_start_offset(&offsets, first_child.start_line, &state.source),
indent,
});
};
}
let (body_start, _) = chunk_region_range(anchor, ChunkRegion::Inner)?;
Ok(InsertionPoint { offset: body_start, indent })
let (body_start, _) = chunk_region_range(anchor, ChunkRegion::Inner);
InsertionPoint { offset: body_start, indent }
}
fn resolve_insertion_point(
@@ -1080,13 +1080,13 @@ fn resolve_insertion_point(
// Inner first-child position
(Some(ChunkRegion::Inner), ChunkEditOp::Before | ChunkEditOp::Prepend)
| (Some(ChunkRegion::Head), ChunkEditOp::After | ChunkEditOp::Append) => Ok((
body_insertion_point(state, anchor, false, file_indent_char, file_indent_step)?,
body_insertion_point(state, anchor, false, file_indent_char, file_indent_step),
InsertPosition::FirstChild,
)),
// Inner last-child position
(Some(ChunkRegion::Inner), ChunkEditOp::After | ChunkEditOp::Append)
| (Some(ChunkRegion::Tail), ChunkEditOp::Before | ChunkEditOp::Prepend) => Ok((
body_insertion_point(state, anchor, true, file_indent_char, file_indent_step)?,
body_insertion_point(state, anchor, true, file_indent_char, file_indent_step),
InsertPosition::LastChild,
)),
(_, ChunkEditOp::Replace | ChunkEditOp::Delete) => {
@@ -1207,35 +1207,22 @@ fn container_has_interior_content(state: &ChunkStateInner, anchor: &ChunkNode) -
/// children (methods) want blank line spacing. Containers whose children are
/// all packed declarations (struct fields, enum variants) are tightly packed.
fn children_want_blank_line_spacing(state: &ChunkStateInner, anchor: &ChunkNode) -> bool {
// Root children are always top-level declarations, separated by blank lines.
if anchor.path.is_empty() {
return true;
}
// If the container has no chunk children, fall back to spaced (preserves
// existing behavior for containers with interior content but no parsed
// children).
if anchor.children.is_empty() {
return true;
}
// Packed children are declarations that belong tightly together without
// blank line separators: struct fields, enum variants, etc.
let all_packed = anchor.children.iter().all(|child_path| {
state
.tree
.chunks
.iter()
.any(|c| c.path == *child_path && is_packed_child(&c.name))
.any(|c| c.path == *child_path && c.kind.traits().packed)
});
!all_packed
}
/// Returns true if a chunk name indicates a packed (tightly-spaced) child.
/// These are declarations like struct fields and enum variants that don't
/// need blank line separators between them.
fn is_packed_child(name: &str) -> bool {
name.starts_with("field_") || name.starts_with("variant_")
}
/// Returns true if sibling insertions around `anchor` should have blank line
/// spacing. Checks whether the anchor's parent container uses spaced or packed
/// layout.
@@ -2088,7 +2075,12 @@ mod tests {
.tree
.chunks
.iter()
.find(|c| c.name.starts_with("stmts"))
.find(|c| {
c.path
.rsplit('.')
.next()
.is_some_and(|leaf| leaf.starts_with("stmts"))
})
.expect("stmts chunk should exist");
assert!(stmts.group, "stmts chunk should be marked as group");
@@ -2419,7 +2411,59 @@ mod tests {
assert_eq!(
result.diff_after, "class Server {\n start() {\n return 42;\n }\n}\n",
"nested body replace should produce correct 2-level indent"
"4-space: nested body replace should produce correct 2-level indent"
);
}
#[test]
fn nested_body_replace_preserves_correct_indentation_2space() {
// 2-space file: method body at 2 levels of indent.
let source = "class Server {\n start() {\n work();\n }\n}\n";
let state = state_for(source, "typescript");
let chunk = state
.inner()
.chunk("class_Server.fn_start")
.expect("fn_start");
let result = apply_single_edit(&state, "test.ts", EditOperation {
op: ChunkEditOp::Replace,
sel: Some(format!("class_Server.fn_start#{}@inner", chunk.checksum)),
crc: None,
region: None,
content: Some("\treturn 42;\n".to_owned()),
find: None,
});
assert_eq!(
result.diff_after, "class Server {\n start() {\n return 42;\n }\n}\n",
"2-space: nested body replace should produce correct 2-level indent"
);
}
#[test]
fn nested_body_replace_with_excess_tabs_corrected() {
// Agent accidentally includes base padding (2 tabs instead of 1).
// Correction mechanism should strip common indent and produce correct output.
let source = "class Server {\n start() {\n work();\n }\n}\n";
let state = state_for(source, "typescript");
let chunk = state
.inner()
.chunk("class_Server.fn_start")
.expect("fn_start");
let result = apply_single_edit(&state, "test.ts", EditOperation {
op: ChunkEditOp::Replace,
sel: Some(format!("class_Server.fn_start#{}@inner", chunk.checksum)),
crc: None,
region: None,
content: Some("\t\tif (x) {\n\t\t\ty();\n\t\t}\n".to_owned()),
find: None,
});
assert_eq!(
result.diff_after,
"class Server {\n start() {\n if (x) {\n y();\n }\n }\n}\n",
"2-space: excess tabs should be corrected via dedent"
);
}
@@ -2492,7 +2536,12 @@ mod tests {
.tree
.chunks
.iter()
.find(|c| c.name.starts_with("list"))
.find(|c| {
c.path
.rsplit('.')
.next()
.is_some_and(|leaf| leaf.starts_with("list"))
})
.expect("list chunk");
let result = apply_single_edit(&state, "test.md", EditOperation {
+11 -2
View File
@@ -509,7 +509,7 @@ fn hashline_prefix_len(line: &str) -> Option<usize> {
#[cfg(test)]
mod tests {
use super::*;
use crate::chunk::types::ChunkNode;
use crate::chunk::{kind::ChunkKind, types::ChunkNode};
fn chunk(
path: &str,
@@ -518,9 +518,18 @@ mod tests {
indent: u32,
indent_char: &str,
) -> ChunkNode {
let kind = match path.split_once('_').map_or(path, |(prefix, _)| prefix) {
"fn" => ChunkKind::Function,
"class" => ChunkKind::Class,
"stmts" => ChunkKind::Statements,
_ => ChunkKind::Chunk,
};
ChunkNode {
path: path.to_owned(),
name: path.to_owned(),
identifier: path
.split_once('_')
.and_then(|(_, identifier)| (!identifier.is_empty()).then_some(identifier.to_owned())),
kind,
leaf: children.is_empty(),
parent_path: parent_path.map(str::to_owned),
children: children.iter().map(|child| (*child).to_owned()).collect(),
+675
View File
@@ -0,0 +1,675 @@
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ChunkKind {
Add,
After,
Alias,
Algo,
Arg,
Argv,
Array,
At,
Attr,
AttrExpr,
Attrs,
Block,
BlockIf,
BlockLocals,
Body,
Case,
Cases,
Catch,
Cell,
Class,
Clause,
Cmd,
Code,
Cond,
Constructor,
Contract,
Copy,
Custom,
Declarations,
Decl,
DefaultExport,
Define,
Directive,
Either,
Elif,
Else,
Enum,
Env,
Error,
Except,
Exports,
Expose,
Expression,
Field,
Fields,
File,
Frame,
Function,
For,
ForIn,
ForOf,
Form,
Frontmatter,
Group,
GroupBy,
Headers,
Healthcheck,
Html,
Hunks,
If,
Iface,
Impl,
Imports,
Includes,
InlineFragment,
Install,
Interface,
Interpolation,
Item,
Join,
Key,
KeyScripts,
Label,
Let,
List,
Loop,
Macro,
Map,
Markdown,
Match,
Method,
Methods,
Module,
Mustache,
Object,
Operation,
Operator,
Option,
Options,
OrderBy,
Parameters,
Preamble,
Proc,
Process,
Project,
Proto,
Py,
Python,
Query,
Receive,
Recipe,
Relations,
Render,
Return,
Root,
Rule,
Schema,
Script,
ScriptModule,
ScriptSetup,
Section,
Select,
Setting,
Shebang,
Shell,
Slot,
Snippet,
Source,
Stage,
StaticInit,
Statements,
Struct,
Style,
StyleScoped,
Switch,
Table,
Tag,
Target,
Template,
Text,
Trait,
Translation,
Try,
Ts,
Type,
Typescript,
Union,
User,
Val,
Variable,
Variant,
Variants,
VersionGate,
When,
Where,
While,
With,
Workdir,
Chunk,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SummaryStyle {
Function,
Variable,
Imports,
Default,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ChunkTraits {
pub groupable: bool,
pub packed: bool,
pub addressable_leaf: bool,
pub always_preserve_children: bool,
pub has_addressable_members: bool,
pub summary: SummaryStyle,
pub container: bool,
}
const DEFAULT_TRAITS: ChunkTraits = ChunkTraits {
groupable: false,
packed: false,
addressable_leaf: false,
always_preserve_children: false,
has_addressable_members: false,
summary: SummaryStyle::Default,
container: false,
};
const GROUP_TRAITS: ChunkTraits = ChunkTraits { groupable: true, ..DEFAULT_TRAITS };
const CONTAINER_TRAITS: ChunkTraits = ChunkTraits { container: true, ..DEFAULT_TRAITS };
const ADDRESSABLE_CONTAINER_TRAITS: ChunkTraits =
ChunkTraits { container: true, has_addressable_members: true, ..DEFAULT_TRAITS };
const PRESERVE_CHILDREN_TRAITS: ChunkTraits =
ChunkTraits { container: true, always_preserve_children: true, ..DEFAULT_TRAITS };
const PACKED_LEAF_TRAITS: ChunkTraits =
ChunkTraits { packed: true, addressable_leaf: true, ..DEFAULT_TRAITS };
const FUNCTION_TRAITS: ChunkTraits =
ChunkTraits { summary: SummaryStyle::Function, ..DEFAULT_TRAITS };
const VARIABLE_TRAITS: ChunkTraits =
ChunkTraits { summary: SummaryStyle::Variable, ..DEFAULT_TRAITS };
const IMPORTS_TRAITS: ChunkTraits =
ChunkTraits { groupable: true, summary: SummaryStyle::Imports, ..DEFAULT_TRAITS };
impl ChunkKind {
pub const fn prefix(self) -> &'static str {
match self {
Self::Add => "add",
Self::After => "after",
Self::Alias => "alias",
Self::Algo => "algo",
Self::Arg => "arg",
Self::Argv => "argv",
Self::Array => "array",
Self::At => "at",
Self::Attr => "attr",
Self::AttrExpr => "attr_expr",
Self::Attrs => "attrs",
Self::Block => "block",
Self::BlockIf => "block_if",
Self::BlockLocals => "block_locals",
Self::Body => "body",
Self::Case => "case",
Self::Cases => "cases",
Self::Catch => "catch",
Self::Cell => "cell",
Self::Class => "class",
Self::Clause => "clause",
Self::Cmd => "cmd",
Self::Code => "code",
Self::Cond => "cond",
Self::Constructor => "constructor",
Self::Contract => "contract",
Self::Copy => "copy",
Self::Custom => "custom",
Self::Declarations => "decls",
Self::Decl => "decl",
Self::DefaultExport => "default_export",
Self::Define => "define",
Self::Directive => "directive",
Self::Either => "either",
Self::Elif => "elif",
Self::Else => "else",
Self::Enum => "enum",
Self::Env => "env",
Self::Error => "error",
Self::Except => "except",
Self::Exports => "exports",
Self::Expose => "expose",
Self::Expression => "expr",
Self::Field => "field",
Self::Fields => "fields",
Self::File => "file",
Self::Frame => "frame",
Self::Function => "fn",
Self::For => "for",
Self::ForIn => "for_in",
Self::ForOf => "for_of",
Self::Form => "form",
Self::Frontmatter => "frontmatter",
Self::Group => "group",
Self::GroupBy => "group_by",
Self::Headers => "headers",
Self::Healthcheck => "healthcheck",
Self::Html => "html",
Self::Hunks => "hunks",
Self::If => "if",
Self::Iface => "iface",
Self::Impl => "impl",
Self::Imports => "imports",
Self::Includes => "includes",
Self::InlineFragment => "inline_fragment",
Self::Install => "install",
Self::Interface => "interface",
Self::Interpolation => "interpolation",
Self::Item => "item",
Self::Join => "join",
Self::Key => "key",
Self::KeyScripts => "key_scripts",
Self::Label => "label",
Self::Let => "let",
Self::List => "list",
Self::Loop => "loop",
Self::Macro => "macro",
Self::Map => "map",
Self::Markdown => "markdown",
Self::Match => "match",
Self::Method => "meth",
Self::Methods => "methods",
Self::Module => "mod",
Self::Mustache => "mustache",
Self::Object => "object",
Self::Operation => "operation",
Self::Operator => "operator",
Self::Option => "option",
Self::Options => "options",
Self::OrderBy => "order_by",
Self::Parameters => "params",
Self::Preamble => "preamble",
Self::Proc => "proc",
Self::Process => "process",
Self::Project => "project",
Self::Proto => "proto",
Self::Py => "py",
Self::Python => "python",
Self::Query => "query",
Self::Receive => "receive",
Self::Recipe => "recipe",
Self::Relations => "relations",
Self::Render => "render",
Self::Return => "ret",
Self::Root => "root",
Self::Rule => "rule",
Self::Schema => "schema",
Self::Script => "script",
Self::ScriptModule => "script_module",
Self::ScriptSetup => "script_setup",
Self::Section => "section",
Self::Select => "select",
Self::Setting => "setting",
Self::Shebang => "shebang",
Self::Shell => "shell",
Self::Slot => "slot",
Self::Snippet => "snippet",
Self::Source => "source",
Self::Stage => "stage",
Self::StaticInit => "static_init",
Self::Statements => "stmts",
Self::Struct => "struct",
Self::Style => "style",
Self::StyleScoped => "style_scoped",
Self::Switch => "switch",
Self::Table => "table",
Self::Tag => "tag",
Self::Target => "target",
Self::Template => "template",
Self::Text => "text",
Self::Trait => "trait",
Self::Translation => "translation",
Self::Try => "try",
Self::Ts => "ts",
Self::Type => "type",
Self::Typescript => "typescript",
Self::Union => "union",
Self::User => "user",
Self::Val => "val",
Self::Variable => "var",
Self::Variant => "variant",
Self::Variants => "variants",
Self::VersionGate => "version_gate",
Self::When => "when",
Self::Where => "where",
Self::While => "while",
Self::With => "with",
Self::Workdir => "workdir",
Self::Chunk => "chunk",
}
}
pub const fn traits(self) -> &'static ChunkTraits {
match self {
Self::Add => &GROUP_TRAITS,
Self::After => &GROUP_TRAITS,
Self::Alias => &DEFAULT_TRAITS,
Self::Algo => &CONTAINER_TRAITS,
Self::Arg => &DEFAULT_TRAITS,
Self::Argv => &DEFAULT_TRAITS,
Self::Array => &DEFAULT_TRAITS,
Self::At => &DEFAULT_TRAITS,
Self::Attr => &DEFAULT_TRAITS,
Self::AttrExpr => &DEFAULT_TRAITS,
Self::Attrs => &CONTAINER_TRAITS,
Self::Block => &DEFAULT_TRAITS,
Self::BlockIf => &DEFAULT_TRAITS,
Self::BlockLocals => &DEFAULT_TRAITS,
Self::Body => &DEFAULT_TRAITS,
Self::Case => &DEFAULT_TRAITS,
Self::Cases => &GROUP_TRAITS,
Self::Catch => &DEFAULT_TRAITS,
Self::Cell => &CONTAINER_TRAITS,
Self::Class => &CONTAINER_TRAITS,
Self::Clause => &DEFAULT_TRAITS,
Self::Cmd => &GROUP_TRAITS,
Self::Code => &GROUP_TRAITS,
Self::Cond => &DEFAULT_TRAITS,
Self::Constructor => &FUNCTION_TRAITS,
Self::Contract => &DEFAULT_TRAITS,
Self::Copy => &GROUP_TRAITS,
Self::Custom => &DEFAULT_TRAITS,
Self::Declarations => &GROUP_TRAITS,
Self::Decl => &DEFAULT_TRAITS,
Self::DefaultExport => &DEFAULT_TRAITS,
Self::Define => &DEFAULT_TRAITS,
Self::Directive => &DEFAULT_TRAITS,
Self::Either => &DEFAULT_TRAITS,
Self::Elif => &DEFAULT_TRAITS,
Self::Else => &DEFAULT_TRAITS,
Self::Enum => &ADDRESSABLE_CONTAINER_TRAITS,
Self::Env => &DEFAULT_TRAITS,
Self::Error => &DEFAULT_TRAITS,
Self::Except => &DEFAULT_TRAITS,
Self::Exports => &GROUP_TRAITS,
Self::Expose => &DEFAULT_TRAITS,
Self::Expression => &DEFAULT_TRAITS,
Self::Field => &PACKED_LEAF_TRAITS,
Self::Fields => &GROUP_TRAITS,
Self::File => &DEFAULT_TRAITS,
Self::Frame => &DEFAULT_TRAITS,
Self::Function => &FUNCTION_TRAITS,
Self::For => &DEFAULT_TRAITS,
Self::ForIn => &DEFAULT_TRAITS,
Self::ForOf => &DEFAULT_TRAITS,
Self::Form => &DEFAULT_TRAITS,
Self::Frontmatter => &DEFAULT_TRAITS,
Self::Group => &DEFAULT_TRAITS,
Self::GroupBy => &DEFAULT_TRAITS,
Self::Headers => &GROUP_TRAITS,
Self::Healthcheck => &DEFAULT_TRAITS,
Self::Html => &GROUP_TRAITS,
Self::Hunks => &GROUP_TRAITS,
Self::If => &DEFAULT_TRAITS,
Self::Iface => &PRESERVE_CHILDREN_TRAITS,
Self::Impl => &CONTAINER_TRAITS,
Self::Imports => &IMPORTS_TRAITS,
Self::Includes => &GROUP_TRAITS,
Self::InlineFragment => &DEFAULT_TRAITS,
Self::Install => &DEFAULT_TRAITS,
Self::Interface => &PRESERVE_CHILDREN_TRAITS,
Self::Interpolation => &GROUP_TRAITS,
Self::Item => &DEFAULT_TRAITS,
Self::Join => &DEFAULT_TRAITS,
Self::Key => &DEFAULT_TRAITS,
Self::KeyScripts => &DEFAULT_TRAITS,
Self::Label => &DEFAULT_TRAITS,
Self::Let => &DEFAULT_TRAITS,
Self::List => &DEFAULT_TRAITS,
Self::Loop => &DEFAULT_TRAITS,
Self::Macro => &DEFAULT_TRAITS,
Self::Map => &DEFAULT_TRAITS,
Self::Markdown => &DEFAULT_TRAITS,
Self::Match => &DEFAULT_TRAITS,
Self::Method => &DEFAULT_TRAITS,
Self::Methods => &GROUP_TRAITS,
Self::Module => &CONTAINER_TRAITS,
Self::Mustache => &DEFAULT_TRAITS,
Self::Object => &DEFAULT_TRAITS,
Self::Operation => &DEFAULT_TRAITS,
Self::Operator => &DEFAULT_TRAITS,
Self::Option => &DEFAULT_TRAITS,
Self::Options => &GROUP_TRAITS,
Self::OrderBy => &DEFAULT_TRAITS,
Self::Parameters => &GROUP_TRAITS,
Self::Preamble => &DEFAULT_TRAITS,
Self::Proc => &CONTAINER_TRAITS,
Self::Process => &CONTAINER_TRAITS,
Self::Project => &DEFAULT_TRAITS,
Self::Proto => &DEFAULT_TRAITS,
Self::Py => &DEFAULT_TRAITS,
Self::Python => &DEFAULT_TRAITS,
Self::Query => &DEFAULT_TRAITS,
Self::Receive => &DEFAULT_TRAITS,
Self::Recipe => &DEFAULT_TRAITS,
Self::Relations => &DEFAULT_TRAITS,
Self::Render => &DEFAULT_TRAITS,
Self::Return => &DEFAULT_TRAITS,
Self::Root => &DEFAULT_TRAITS,
Self::Rule => &DEFAULT_TRAITS,
Self::Schema => &DEFAULT_TRAITS,
Self::Script => &DEFAULT_TRAITS,
Self::ScriptModule => &DEFAULT_TRAITS,
Self::ScriptSetup => &DEFAULT_TRAITS,
Self::Section => &DEFAULT_TRAITS,
Self::Select => &DEFAULT_TRAITS,
Self::Setting => &DEFAULT_TRAITS,
Self::Shebang => &DEFAULT_TRAITS,
Self::Shell => &DEFAULT_TRAITS,
Self::Slot => &DEFAULT_TRAITS,
Self::Snippet => &DEFAULT_TRAITS,
Self::Source => &DEFAULT_TRAITS,
Self::Stage => &DEFAULT_TRAITS,
Self::StaticInit => &DEFAULT_TRAITS,
Self::Statements => &GROUP_TRAITS,
Self::Struct => &ADDRESSABLE_CONTAINER_TRAITS,
Self::Style => &DEFAULT_TRAITS,
Self::StyleScoped => &DEFAULT_TRAITS,
Self::Switch => &DEFAULT_TRAITS,
Self::Table => &DEFAULT_TRAITS,
Self::Tag => &DEFAULT_TRAITS,
Self::Target => &DEFAULT_TRAITS,
Self::Template => &DEFAULT_TRAITS,
Self::Text => &GROUP_TRAITS,
Self::Trait => &PRESERVE_CHILDREN_TRAITS,
Self::Translation => &DEFAULT_TRAITS,
Self::Try => &DEFAULT_TRAITS,
Self::Ts => &DEFAULT_TRAITS,
Self::Type => &ADDRESSABLE_CONTAINER_TRAITS,
Self::Typescript => &DEFAULT_TRAITS,
Self::Union => &DEFAULT_TRAITS,
Self::User => &DEFAULT_TRAITS,
Self::Val => &DEFAULT_TRAITS,
Self::Variable => &VARIABLE_TRAITS,
Self::Variant => &PACKED_LEAF_TRAITS,
Self::Variants => &DEFAULT_TRAITS,
Self::VersionGate => &DEFAULT_TRAITS,
Self::When => &DEFAULT_TRAITS,
Self::Where => &DEFAULT_TRAITS,
Self::While => &DEFAULT_TRAITS,
Self::With => &GROUP_TRAITS,
Self::Workdir => &DEFAULT_TRAITS,
Self::Chunk => &DEFAULT_TRAITS,
}
}
pub fn path_segment(self, identifier: Option<&str>) -> String {
match identifier {
Some(identifier) => format!("{}_{identifier}", self.prefix()),
None => self.prefix().to_string(),
}
}
pub fn from_sanitized_kind(kind: &str) -> Self {
match kind {
"add" => Self::Add,
"after" => Self::After,
"alias" => Self::Alias,
"algo" => Self::Algo,
"arg" => Self::Arg,
"argv" => Self::Argv,
"array" => Self::Array,
"at" => Self::At,
"attr" => Self::Attr,
"attr_expr" => Self::AttrExpr,
"attrs" => Self::Attrs,
"block" => Self::Block,
"block_if" => Self::BlockIf,
"block_locals" => Self::BlockLocals,
"body" => Self::Body,
"case" => Self::Case,
"cases" => Self::Cases,
"catch" => Self::Catch,
"cell" => Self::Cell,
"class" => Self::Class,
"clause" => Self::Clause,
"cmd" => Self::Cmd,
"code" => Self::Code,
"cond" => Self::Cond,
"constructor" => Self::Constructor,
"contract" => Self::Contract,
"copy" => Self::Copy,
"custom" => Self::Custom,
"decls" => Self::Declarations,
"decl" => Self::Decl,
"default_export" => Self::DefaultExport,
"define" => Self::Define,
"directive" => Self::Directive,
"either" => Self::Either,
"elif" => Self::Elif,
"else" => Self::Else,
"enum" => Self::Enum,
"env" => Self::Env,
"error" => Self::Error,
"except" => Self::Except,
"exports" => Self::Exports,
"expose" => Self::Expose,
"expr" => Self::Expression,
"field" => Self::Field,
"fields" => Self::Fields,
"file" => Self::File,
"frame" => Self::Frame,
"fn" => Self::Function,
"for" => Self::For,
"for_in" => Self::ForIn,
"for_of" => Self::ForOf,
"form" => Self::Form,
"frontmatter" => Self::Frontmatter,
"group" => Self::Group,
"group_by" => Self::GroupBy,
"headers" => Self::Headers,
"healthcheck" => Self::Healthcheck,
"html" => Self::Html,
"hunks" => Self::Hunks,
"if" => Self::If,
"iface" => Self::Iface,
"impl" => Self::Impl,
"imports" => Self::Imports,
"includes" => Self::Includes,
"inline_fragment" => Self::InlineFragment,
"install" => Self::Install,
"interface" => Self::Interface,
"interpolation" => Self::Interpolation,
"item" => Self::Item,
"join" => Self::Join,
"key" => Self::Key,
"key_scripts" => Self::KeyScripts,
"label" => Self::Label,
"let" => Self::Let,
"list" => Self::List,
"loop" => Self::Loop,
"macro" => Self::Macro,
"map" => Self::Map,
"markdown" => Self::Markdown,
"match" => Self::Match,
"meth" => Self::Method,
"methods" => Self::Methods,
"mod" => Self::Module,
"mustache" => Self::Mustache,
"object" => Self::Object,
"operation" => Self::Operation,
"operator" => Self::Operator,
"option" => Self::Option,
"options" => Self::Options,
"order_by" => Self::OrderBy,
"params" => Self::Parameters,
"preamble" => Self::Preamble,
"proc" => Self::Proc,
"process" => Self::Process,
"project" => Self::Project,
"proto" => Self::Proto,
"py" => Self::Py,
"python" => Self::Python,
"query" => Self::Query,
"receive" => Self::Receive,
"recipe" => Self::Recipe,
"relations" => Self::Relations,
"render" => Self::Render,
"ret" => Self::Return,
"root" => Self::Root,
"rule" => Self::Rule,
"schema" => Self::Schema,
"script" => Self::Script,
"script_module" => Self::ScriptModule,
"script_setup" => Self::ScriptSetup,
"section" => Self::Section,
"select" => Self::Select,
"setting" => Self::Setting,
"shebang" => Self::Shebang,
"shell" => Self::Shell,
"slot" => Self::Slot,
"snippet" => Self::Snippet,
"source" => Self::Source,
"stage" => Self::Stage,
"static_init" => Self::StaticInit,
"stmts" => Self::Statements,
"struct" => Self::Struct,
"style" => Self::Style,
"style_scoped" => Self::StyleScoped,
"switch" => Self::Switch,
"table" => Self::Table,
"tag" => Self::Tag,
"target" => Self::Target,
"template" => Self::Template,
"text" => Self::Text,
"trait" => Self::Trait,
"translation" => Self::Translation,
"try" => Self::Try,
"ts" => Self::Ts,
"type" => Self::Type,
"typescript" => Self::Typescript,
"union" => Self::Union,
"user" => Self::User,
"val" => Self::Val,
"var" => Self::Variable,
"variant" => Self::Variant,
"variants" => Self::Variants,
"version_gate" => Self::VersionGate,
"when" => Self::When,
"where" => Self::Where,
"while" => Self::While,
"with" => Self::With,
"workdir" => Self::Workdir,
"chunk" => Self::Chunk,
_ => Self::Chunk,
}
}
}
+56 -47
View File
@@ -18,6 +18,8 @@ pub(crate) mod resolve;
pub(crate) mod state;
pub mod types;
pub mod kind;
// Per-language classifiers
mod ast_astro;
mod ast_bash_make_diff;
@@ -65,6 +67,7 @@ use xxhash_rust::xxh64::xxh64;
use self::{
classify::{LangClassifier, classifier_for},
common::*,
kind::ChunkKind,
};
pub use self::{
state::ChunkState,
@@ -76,8 +79,8 @@ use crate::{chunk::types::ChunkAnchorStyle, language::SupportLang};
/// Format one chunk anchor string for a node at `depth` using `style` and
/// optional checksum omission.
#[napi(js_name = "formatAnchor")]
pub fn format_anchor_napi(
#[napi]
pub fn format_anchor(
name: String,
checksum: String,
style: ChunkAnchorStyle,
@@ -125,7 +128,8 @@ pub(crate) fn build_chunk_tree(source: &str, language: &str) -> Result<ChunkTree
acc.chunks.insert(0, ChunkNode {
path: String::new(),
name: "root".to_string(),
identifier: None,
kind: ChunkKind::Root,
leaf: false,
parent_path: None,
children: root_children.clone(),
@@ -202,7 +206,8 @@ fn build_blank_line_tree(
) -> ChunkTree {
let mut chunks = vec![ChunkNode {
path: String::new(),
name: "root".to_string(),
identifier: None,
kind: ChunkKind::Root,
leaf: false,
parent_path: None,
children: Vec::new(),
@@ -250,7 +255,8 @@ fn build_blank_line_tree(
root_children.push(name.clone());
chunks.push(ChunkNode {
path: name.clone(),
name: name.clone(),
identifier: Some(name.clone()),
kind: ChunkKind::Chunk,
leaf: true,
parent_path: Some(String::new()),
children: Vec::new(),
@@ -302,10 +308,11 @@ fn build_chunk(
acc: &mut ChunkAccumulator,
classifier: &dyn classify::LangClassifier,
) -> String {
let segment = candidate.kind.path_segment(candidate.identifier.as_deref());
let path = if parent_path.is_empty() {
candidate.base_name.clone()
segment
} else {
format!("{parent_path}.{}", candidate.base_name)
format!("{parent_path}.{segment}")
};
let line_count = candidate
.range_end_line
@@ -331,10 +338,12 @@ fn build_chunk(
&& recurse.is_some()
&& recurse_parse_errors == 0
&& should_collapse_trivial_children(&candidate, &child_candidates);
let always_recurse = *ALWAYS_RECURSE && !candidate.groupable && !child_candidates.is_empty();
let should_recurse = !candidate.error
&& recurse.is_some()
&& !should_collapse
&& (candidate.force_recurse
|| always_recurse
|| recurse_parse_errors > 0
|| (line_count > *LEAF_THRESHOLD
&& recursion_narrows_scope(line_count, &child_candidates)));
@@ -351,7 +360,8 @@ fn build_chunk(
let (indent, indent_char) = detect_indent(source, candidate.range_start_byte);
acc.chunks.push(ChunkNode {
path: path.clone(),
name: candidate.base_name,
identifier: candidate.identifier,
kind: candidate.kind,
leaf,
parent_path: Some(parent_path.to_string()),
children,
@@ -432,15 +442,7 @@ fn classify_node<'tree>(
classifier: &dyn LangClassifier,
) -> RawChunkCandidate<'tree> {
if node.is_error() || node.kind() == "ERROR" {
return make_candidate(
node,
"<error>".to_string(),
NameStyle::Error,
None,
None,
false,
source,
);
return make_candidate(node, ChunkKind::Error, None, NameStyle::Error, None, None, source);
}
// Try language-specific classifier first, then fall back to defaults.
@@ -499,7 +501,8 @@ fn group_candidates(candidates: Vec<RawChunkCandidate<'_>>) -> Vec<RawChunkCandi
let next_line_count = line_span(candidate.range_start_line, candidate.range_end_line);
let can_merge = last.groupable
&& candidate.groupable
&& last.base_name == candidate.base_name
&& last.kind == candidate.kind
&& last.identifier == candidate.identifier
&& !candidate.has_leading_comment
&& candidate.range_start_line <= last.range_end_line + 1
&& last_line_count + next_line_count <= *MAX_CHUNK_LINES;
@@ -518,36 +521,44 @@ fn group_candidates(candidates: Vec<RawChunkCandidate<'_>>) -> Vec<RawChunkCandi
fn assign_unique_names(mut candidates: Vec<RawChunkCandidate<'_>>) -> Vec<RawChunkCandidate<'_>> {
let mut totals = HashMap::<String, usize>::new();
for candidate in &candidates {
*totals.entry(candidate.base_name.clone()).or_insert(0) += 1;
let key = candidate.kind.path_segment(candidate.identifier.as_deref());
*totals.entry(key).or_insert(0) += 1;
}
let mut seen = HashMap::<String, usize>::new();
for candidate in &mut candidates {
let count = seen.entry(candidate.base_name.clone()).or_insert(0);
let key = candidate.kind.path_segment(candidate.identifier.as_deref());
let count = seen.entry(key.clone()).or_insert(0);
*count += 1;
let occurrence = *count;
let total = *totals.get(candidate.base_name.as_str()).unwrap_or(&1);
let total = *totals.get(key.as_str()).unwrap_or(&1);
candidate.base_name = match candidate.name_style {
candidate.identifier = match candidate.name_style {
NameStyle::Error => {
if total > 1 {
format!("error_{occurrence}")
Some(occurrence.to_string())
} else {
"error".to_string()
None
}
},
NameStyle::Named => {
if total > 1 {
format!("{}_{}", candidate.base_name, occurrence)
match candidate.identifier.as_deref() {
Some(identifier) => Some(format!("{identifier}_{occurrence}")),
None => Some(occurrence.to_string()),
}
} else {
candidate.base_name.clone()
candidate.identifier.clone()
}
},
NameStyle::Group => {
if total == 1 || occurrence == 1 {
candidate.base_name.clone()
candidate.identifier.clone()
} else {
format!("{}_{}", candidate.base_name, occurrence)
match candidate.identifier.as_deref() {
Some(identifier) => Some(format!("{identifier}_{occurrence}")),
None => Some(occurrence.to_string()),
}
}
},
};
@@ -582,19 +593,11 @@ fn should_collapse_trivial_children(
return false;
}
let has_addressable_leaf_members = children.iter().all(|child| {
child.base_name.starts_with("field_") || child.base_name.starts_with("variant_")
});
if has_addressable_leaf_members
&& (parent.base_name.starts_with("struct_")
|| parent.base_name.starts_with("enum_")
|| parent.base_name.starts_with("type_"))
{
let has_addressable_leaf_members = children.iter().all(|child| child.kind.traits().packed);
if has_addressable_leaf_members && parent.kind.traits().has_addressable_members {
return false;
}
// Trait and interface members should always be addressable so that
// individual method signatures can be edited.
if parent.base_name.starts_with("trait_") || parent.base_name.starts_with("interface_") {
if parent.kind.traits().always_preserve_children {
return false;
}
@@ -738,7 +741,8 @@ fn insert_preamble_chunk(
);
let preamble = ChunkNode {
path: "preamble".to_string(),
name: "preamble".to_string(),
identifier: None,
kind: ChunkKind::Preamble,
leaf: true,
parent_path: Some(String::new()),
children: Vec::new(),
@@ -772,7 +776,7 @@ mod tests {
state::ChunkState,
types::{ChunkAnchorStyle, ReadRenderParams},
};
use crate::language::SupportLang;
use crate::{chunk::ChunkKind, language::SupportLang};
fn assert_supported_sample(language: &str, source: &str) {
let tree = build_chunk_tree(source, language)
@@ -1044,7 +1048,12 @@ function main(): void {{
let tree = build_chunk_tree(source, "typescript").expect("tree should build");
assert!(tree.parse_errors > 0);
assert!(tree.chunks.iter().any(|chunk| chunk.name == "error"));
assert!(
tree
.chunks
.iter()
.any(|chunk| chunk.kind == ChunkKind::Error && chunk.identifier.is_none())
);
}
#[test]
@@ -1411,15 +1420,15 @@ impl Config {
let attrset = tree
.chunks
.iter()
.find(|chunk| chunk.path == "attrset_expr")
.expect("attrset_expr chunk");
.find(|chunk| chunk.path == "attrs")
.expect("attrs chunk");
assert!(!tree.fallback, "nix should use tree-sitter chunking");
assert!(!attrset.leaf, "top-level attrset should recurse into bindings");
assert!(
attrset
.children
.iter()
.any(|child| child == "attrset_expr.attr_hello"),
.any(|child| child == "attrs.attr_hello"),
"expected attr_hello child, got {:?}",
attrset.children
);
@@ -1427,7 +1436,7 @@ impl Config {
attrset
.children
.iter()
.any(|child| child == "attrset_expr.attr_nested"),
.any(|child| child == "attrs.attr_nested"),
"expected attr_nested child, got {:?}",
attrset.children
);
@@ -1624,7 +1633,7 @@ impl Config {
.expect("listing should succeed");
assert!(result.text.contains("sample.ts chunks:"));
assert!(result.text.contains("fn_run#"));
assert!(result.text.contains("regions: outer, head, inner, tail"));
// Region listing removed — all chunks accept all regions now.
assert!(!result.text.contains("return 1"));
}
+12 -6
View File
@@ -275,13 +275,13 @@ fn chunk_body_anchor_indent(
})
}
const fn chunk_anchor_label(chunk: &ChunkNode, style: ChunkAnchorStyle) -> &str {
fn chunk_anchor_label(chunk: &ChunkNode, style: ChunkAnchorStyle) -> String {
match style {
ChunkAnchorStyle::Full | ChunkAnchorStyle::FullOmit => chunk.path.as_str(),
ChunkAnchorStyle::Full | ChunkAnchorStyle::FullOmit => chunk.path.clone(),
ChunkAnchorStyle::Kind
| ChunkAnchorStyle::KindOmit
| ChunkAnchorStyle::Bare
| ChunkAnchorStyle::None => chunk.name.as_str(),
| ChunkAnchorStyle::None => chunk.kind.path_segment(chunk.identifier.as_deref()),
}
}
@@ -700,7 +700,10 @@ fn emit_chunk_subtree(
);
let style = ctx.anchor_style.with_omit_checksum(ctx.omit_checksum);
let anchor_label = chunk_anchor_label(chunk, style);
push_meta(ctx, style.render(&anchor_indent, anchor_label, chunk.checksum.as_str()));
push_meta(
ctx,
style.render(&anchor_indent, anchor_label.as_str(), chunk.checksum.as_str()),
);
return;
},
Some(ChunkFocusMode::Container | ChunkFocusMode::Expanded) => {
@@ -730,7 +733,7 @@ fn emit_chunk_subtree(
);
let style = ctx.anchor_style.with_omit_checksum(ctx.omit_checksum);
let anchor_label = chunk_anchor_label(chunk, style);
push_meta(ctx, style.render(&anchor_indent, anchor_label, chunk.checksum.as_str()));
push_meta(ctx, style.render(&anchor_indent, anchor_label.as_str(), chunk.checksum.as_str()));
}
if !has_kids {
if ctx.show_leaf_preview
@@ -784,7 +787,10 @@ fn emit_chunk_subtree(
);
let style = ctx.anchor_style.with_omit_checksum(ctx.omit_checksum);
let anchor_label = chunk_anchor_label(chunk, style);
push_meta(ctx, style.render_close(&anchor_indent, anchor_label, chunk.checksum.as_str()));
push_meta(
ctx,
style.render_close(&anchor_indent, anchor_label.as_str(), chunk.checksum.as_str()),
);
}
return;
}
+47 -65
View File
@@ -5,8 +5,6 @@ use crate::chunk::{
types::{ChunkNode, ChunkRegion, ChunkTree},
};
const CHUNK_NAME_PREFIXES: &[&str] =
&["fn_", "var_", "class_", "stmts_", "type_", "interface_", "enum_", "const_"];
const CHECKSUM_ALPHABET: &str = "ZPMQVRWSNKTXJBYH";
pub struct ResolvedChunk<'a> {
@@ -196,39 +194,15 @@ fn root_chunk(state: &ChunkStateInner) -> Result<&ChunkNode, String> {
.ok_or_else(|| "Chunk tree is missing the root chunk".to_owned())
}
pub const fn chunk_supports_region(chunk: &ChunkNode, region: ChunkRegion) -> bool {
pub fn chunk_region_range(chunk: &ChunkNode, region: ChunkRegion) -> (usize, usize) {
let start = chunk.start_byte as usize;
let end = chunk.end_byte as usize;
let pro_end = chunk.prologue_end_byte.map_or(start, |b| b as usize);
let epi_start = chunk.epilogue_start_byte.map_or(end, |b| b as usize);
match region {
ChunkRegion::Head | ChunkRegion::Inner | ChunkRegion::Tail => {
chunk.prologue_end_byte.is_some() && chunk.epilogue_start_byte.is_some()
},
}
}
pub fn chunk_region_range(
chunk: &ChunkNode,
region: ChunkRegion,
) -> Result<(usize, usize), String> {
match region {
ChunkRegion::Head => Ok((
chunk.start_byte as usize,
chunk
.prologue_end_byte
.ok_or_else(|| format!("Chunk \"{}\" does not support @head.", chunk.path))? as usize,
)),
ChunkRegion::Inner => Ok((
chunk
.prologue_end_byte
.ok_or_else(|| format!("Chunk \"{}\" does not support @inner.", chunk.path))? as usize,
chunk
.epilogue_start_byte
.ok_or_else(|| format!("Chunk \"{}\" does not support @inner.", chunk.path))? as usize,
)),
ChunkRegion::Tail => Ok((
chunk
.epilogue_start_byte
.ok_or_else(|| format!("Chunk \"{}\" does not support @tail.", chunk.path))? as usize,
chunk.end_byte as usize,
)),
ChunkRegion::Head => (start, pro_end),
ChunkRegion::Inner => (pro_end, epi_start),
ChunkRegion::Tail => (epi_start, end),
}
}
@@ -289,12 +263,14 @@ fn resolve_chunk_selector_impl<'a>(
}
if !cleaned.contains('.') {
let prefixed = CHUNK_NAME_PREFIXES
.iter()
.map(|prefix| format!("{prefix}{cleaned}"))
.collect::<Vec<_>>();
let prefixed_matches =
collect_unique_matches(prefixed.iter().flat_map(|name| state.chunks_by_leaf(name)));
let prefixed_matches = collect_unique_matches(state.tree.chunks.iter().filter(|chunk| {
if chunk.path.is_empty() {
return false;
}
let leaf = chunk.path.rsplit('.').next().unwrap_or(chunk.path.as_str());
let expected = chunk.kind.path_segment(Some(cleaned));
leaf == expected || leaf == cleaned
}));
if !prefixed_matches.is_empty() {
return resolve_matches(
prefixed_matches,
@@ -472,17 +448,12 @@ fn kind_path_matches(candidate: &ChunkNode, kind_segments: &[&str]) -> bool {
.zip(path_segments)
.all(|(kind, segment)| {
segment == *kind
|| segment.starts_with(&format!("{kind}_"))
|| strip_known_chunk_prefix(segment) == Some(*kind)
|| segment
.split_once('_')
.is_some_and(|(prefix, identifier)| prefix == *kind || identifier == *kind)
})
}
fn strip_known_chunk_prefix(segment: &str) -> Option<&str> {
CHUNK_NAME_PREFIXES
.iter()
.find_map(|prefix| segment.strip_prefix(prefix))
}
/// Format a chunk path with its CRC suffix, e.g. `fn_start#ABCD`.
fn format_chunk_ref(tree: &ChunkTree, path: &str) -> String {
if let Some(chunk) = find_chunk_by_path(tree, path) {
@@ -678,6 +649,7 @@ fn chunk_read_path_separator_index(value: &str) -> Option<usize> {
#[cfg(test)]
mod tests {
use super::*;
use crate::chunk::kind::ChunkKind;
fn chunk(
path: &str,
@@ -685,26 +657,36 @@ mod tests {
parent_path: Option<&str>,
children: Vec<&str>,
) -> ChunkNode {
let leaf = path.rsplit('.').next().unwrap_or(path);
let kind = match leaf.split_once('_').map_or(leaf, |(prefix, _)| prefix) {
"fn" => ChunkKind::Function,
"class" => ChunkKind::Class,
"try" => ChunkKind::Try,
_ => ChunkKind::Chunk,
};
ChunkNode {
path: path.to_owned(),
name: path.rsplit('.').next().unwrap_or(path).to_owned(),
leaf: children.is_empty(),
parent_path: parent_path.map(str::to_owned),
children: children.into_iter().map(str::to_owned).collect(),
signature: None,
start_line: 1,
end_line: 1,
line_count: 1,
start_byte: 0,
end_byte: 0,
path: path.to_owned(),
identifier: leaf
.split_once('_')
.and_then(|(_, identifier)| (!identifier.is_empty()).then_some(identifier.to_owned())),
kind,
leaf: children.is_empty(),
parent_path: parent_path.map(str::to_owned),
children: children.into_iter().map(str::to_owned).collect(),
signature: None,
start_line: 1,
end_line: 1,
line_count: 1,
start_byte: 0,
end_byte: 0,
checksum_start_byte: 0,
prologue_end_byte: None,
prologue_end_byte: None,
epilogue_start_byte: None,
checksum: checksum.to_owned(),
error: false,
indent: 0,
indent_char: " ".to_owned(),
group: false,
checksum: checksum.to_owned(),
error: false,
indent: 0,
indent_char: " ".to_owned(),
group: false,
}
}
+8 -42
View File
@@ -11,8 +11,8 @@ use super::{
build_chunk_tree,
indent::{detect_file_indent_char, detect_file_indent_step, normalize_to_tabs},
resolve::{
ParsedSelector, chunk_region_range, chunk_supports_region, format_region_ref,
resolve_chunk_selector, resolve_chunk_with_crc, split_selector_crc_and_region,
ParsedSelector, chunk_region_range, format_region_ref, resolve_chunk_selector,
resolve_chunk_with_crc, split_selector_crc_and_region,
},
};
use crate::chunk::types::{
@@ -273,8 +273,8 @@ impl ChunkState {
}
/// Look up [`ChunkInfo`] for a chunk selector path.
#[napi(js_name = "chunk")]
pub fn chunk_info_for_path(&self, chunk_path: String) -> Option<ChunkInfo> {
#[napi]
pub fn chunk(&self, chunk_path: String) -> Option<ChunkInfo> {
let mut warnings = Vec::new();
resolve_chunk_selector(self.inner(), Some(chunk_path.as_str()), &mut warnings)
.ok()
@@ -413,14 +413,9 @@ impl ChunkState {
if selector.as_deref() == Some("?") {
let mut lines = vec![format!("{} chunks:", params.display_path)];
for chunk in self.inner.chunks().filter(|chunk| !chunk.path.is_empty()) {
let supported_regions = if chunk_supports_region(chunk, ChunkRegion::Inner) {
"outer, head, inner, tail"
} else {
"outer"
};
lines.push(format!(
" {}#{} L{}-L{} regions: {}",
chunk.path, chunk.checksum, chunk.start_line, chunk.end_line, supported_regions
" {}#{} L{}-L{}",
chunk.path, chunk.checksum, chunk.start_line, chunk.end_line,
));
}
return Ok(ReadResult { text: lines.join("\n"), chunk: None });
@@ -448,24 +443,6 @@ impl ChunkState {
// selector and loses the region suffix.
let selector_ref = format_region_ref(chunk, region);
if let Some(r) = region
&& !chunk_supports_region(chunk, r)
{
return Ok(ReadResult {
text: format!(
"{}:{}\n\nChunk \"{}\" does not support @{}.",
params.display_path,
chunk.path,
chunk.path,
r.as_str(),
),
chunk: Some(ChunkReadTarget {
status: ChunkReadStatus::UnsupportedRegion,
selector: selector_ref,
}),
});
}
if let Some(absolute_line_range) = params.absolute_line_range {
let req_start = absolute_line_range.start_line;
let req_end = absolute_line_range.end_line;
@@ -492,18 +469,7 @@ impl ChunkState {
if let Some(target_region) = region {
let masked_source = mask_chunk_display_source(self.inner.source(), self.inner.language());
let (start, end) = match chunk_region_range(chunk, target_region) {
Ok(range) => range,
Err(err) => {
return Ok(ReadResult {
text: format!("{}\n\n{}", params.display_path, err),
chunk: Some(ChunkReadTarget {
status: ChunkReadStatus::UnsupportedRegion,
selector: selector_ref,
}),
});
},
};
let (start, end) = chunk_region_range(chunk, target_region);
let tab_replacement = params.tab_replacement.as_deref().unwrap_or(" ");
let normalize_indent = params.normalize_indent.unwrap_or(false).then(|| {
(
@@ -598,7 +564,7 @@ fn normalize_language(language: &str) -> String {
fn chunk_info(chunk: &ChunkNode) -> ChunkInfo {
ChunkInfo {
path: chunk.path.clone(),
name: chunk.name.clone(),
identifier: chunk.identifier.clone(),
checksum: chunk.checksum.clone(),
start_line: chunk.start_line,
end_line: chunk.end_line,
+3 -3
View File
@@ -7,7 +7,7 @@ use crate::chunk::{kind::ChunkKind, state::ChunkState};
#[derive(Clone)]
pub struct ChunkNode {
pub path: String,
pub name: String,
pub identifier: Option<String>,
pub kind: ChunkKind,
pub leaf: bool,
pub parent_path: Option<String>,
@@ -58,8 +58,8 @@ pub struct ChunkTree {
pub struct ChunkInfo {
/// Chunk selector path within the tree.
pub path: String,
/// Short display name for the chunk (e.g. symbol or region label).
pub name: String,
/// Bare chunk identifier (without kind prefix), if available.
pub identifier: Option<String>,
/// Stable checksum anchor for this chunk.
pub checksum: String,
/// 1-based start line in the source file (inclusive).
+1 -1
View File
@@ -1390,7 +1390,7 @@ fn grep_sync(
let use_gitignore = options.gitignore.unwrap_or(true);
let use_cache = options.cache.unwrap_or(false);
let glob_set = glob_util::try_compile_glob(options.glob.as_deref(), true)?;
let type_filter = resolve_type_filter(options.r#type.as_deref());
let type_filter = resolve_type_filter(options.type_filter.as_deref());
let params = SearchParams {
context_before,
+1 -1
View File
@@ -1,7 +1,6 @@
# Changelog
## [Unreleased]
### Breaking Changes
- Changed `ChunkRegion` enum values from `Container`, `Prologue`, `Body`, `Epilogue` to `Head`, `Inner`, `Tail` with updated semantics for region targeting
@@ -28,6 +27,7 @@
### Changed
- Changed `ChunkInfo.name` field to optional `identifier` field — now provides bare chunk identifier without kind prefix instead of display name
- Updated `region` parameter documentation in `EditOperation` to clarify full chunk targeting when omitted instead of container-scoped default
- Updated `ChunkEditOp` documentation to reflect region-scoped semantics — operations now target specific regions rather than chunk structure positions
- Changed `ChunkEditOp.Replace` documentation to clarify substring replacement via `find` parameter instead of line-based replacement
+2 -2
View File
@@ -412,8 +412,8 @@ export declare enum ChunkFocusMode {
export interface ChunkInfo {
/** Chunk selector path within the tree. */
path: string
/** Short display name for the chunk (e.g. symbol or region label). */
name: string
/** Bare chunk identifier (without kind prefix), if available. */
identifier?: string
/** Stable checksum anchor for this chunk. */
checksum: string
/** 1-based start line in the source file (inclusive). */