use tree_sitter::Node;
use super::intelligence::{node_text, span_between_from};
use super::types::*;
use super::walk::{Descend, walk_bounded, warn_if_truncated};
pub(super) fn structure(root: &Node<'_>, source: &str) -> Vec<StructureItem> {
let mut items = Vec::new();
let mut parsed: Vec<(usize, usize)> = Vec::new();
let truncated = walk_bounded(root, |node, _depth| {
if !node.kind().starts_with("create_") {
return Descend::Children;
}
if let Some(item) = declaration(node, source) {
parsed.push((item.span.start_byte, item.span.end_byte));
items.push(item);
}
Descend::Skip
});
warn_if_truncated(truncated, "intel::sql", "sql");
let leaves: Vec<Node<'_>> = leaves(root).collect();
for (index, leaf) in leaves.iter().enumerate() {
if leaf.kind() != "keyword_create"
|| parsed
.iter()
.any(|(start, end)| (start..end).contains(&&leaf.start_byte()))
{
continue;
}
let Some(head) = read_head(&leaves, index, source) else {
continue;
};
let Some(end) = recovered_end(&leaves, head.consumed, head.routine) else {
continue;
};
items.push(item(head, leaf, end, source));
}
items.sort_by_key(|item| item.span.start_byte);
items
}
fn declaration(node: &Node<'_>, source: &str) -> Option<StructureItem> {
if !matches!(
node.kind(),
"create_table"
| "create_view"
| "create_materialized_view"
| "create_function"
| "create_procedure"
| "create_type"
| "create_index"
| "create_trigger"
| "create_policy"
| "create_schema"
| "create_database"
| "create_role"
) {
return None;
}
let leaves: Vec<Node<'_>> = leaves(node).collect();
let head = read_head(&leaves, 0, source)?;
let after_node = node
.parent()
.filter(|parent| parent.kind() == "statement")
.and_then(|parent| parent.next_sibling())
.filter(|sibling| sibling.kind() == ";")
.map_or(node.end_byte(), |terminator| terminator.end_byte());
let end = match recovered_end(&leaves, head.consumed, head.routine) {
Some(inside) if inside < node.end_byte() => inside,
_ => after_node,
};
Some(item(head, node, end, source))
}
struct Head {
kind: StructureKind,
name: String,
consumed: usize,
routine: bool,
}
fn read_head(leaves: &[Node<'_>], start: usize, source: &str) -> Option<Head> {
let mut index = start + 1;
let mut object = None;
while index < leaves.len() {
let kind = leaves[index].kind();
index += 1;
match kind {
"comment"
| "keyword_or"
| "keyword_replace"
| "keyword_unique"
| "keyword_temp"
| "keyword_temporary"
| "keyword_materialized"
| "keyword_unlogged" => {}
keyword if keyword.starts_with("keyword_") => {
object = Some(keyword);
break;
}
_ => return None,
}
}
let object = object?;
let kind = match object {
"keyword_table" | "keyword_view" => StructureKind::Struct,
"keyword_function" | "keyword_procedure" => StructureKind::Function,
"keyword_type" => StructureKind::Other("Type".to_string()),
"keyword_index" => StructureKind::Other("Index".to_string()),
"keyword_trigger" => StructureKind::Other("Trigger".to_string()),
"keyword_policy" => StructureKind::Other("Policy".to_string()),
"keyword_schema" | "keyword_database" => StructureKind::Module,
"keyword_role" => StructureKind::Other("Role".to_string()),
_ => return None,
};
while index < leaves.len()
&& matches!(
leaves[index].kind(),
"comment" | "keyword_if" | "keyword_not" | "keyword_exists" | "keyword_concurrently"
)
{
index += 1;
}
let mut name = qualified_name(leaves, &mut index, source)?;
if matches!(object, "keyword_trigger" | "keyword_policy") {
index = leaves
.get(index..)?
.iter()
.position(|leaf| leaf.kind() == "keyword_on")?
+ index
+ 1;
let table = qualified_name(leaves, &mut index, source)?;
name = format!("{table}.{name}");
}
Some(Head {
kind,
name,
consumed: index,
routine: matches!(object, "keyword_function" | "keyword_procedure"),
})
}
fn qualified_name(leaves: &[Node<'_>], index: &mut usize, source: &str) -> Option<String> {
if (*index)
.checked_sub(1)
.and_then(|before| leaves.get(before))
.is_some_and(|before| node_text(before, source) == ":")
{
return None;
}
let mut parts = Vec::new();
while let Some(leaf) = leaves.get(*index) {
let dotted = leaves.get(*index + 1).is_some_and(|next| next.kind() == ".");
if leaf.kind() != "identifier" && !(leaf.kind().starts_with("keyword_") && dotted) {
break;
}
parts.push(plain(node_text(leaf, source)));
*index += 1;
if !dotted {
break;
}
*index += 1;
}
if parts.is_empty() { None } else { Some(parts.join(".")) }
}
fn plain(identifier: &str) -> &str {
identifier
.strip_prefix('"')
.and_then(|inner| inner.strip_suffix('"'))
.unwrap_or(identifier)
}
fn recovered_end(leaves: &[Node<'_>], from: usize, routine: bool) -> Option<usize> {
if routine && let Some(close) = routine_close(leaves, from) {
return Some(terminated(leaves, close));
}
let mut end = None;
for leaf in &leaves[from..] {
match leaf.kind() {
";" => return Some(leaf.end_byte()),
"keyword_create" => break,
_ => end = Some(leaf.end_byte()),
}
}
end
}
fn routine_close(leaves: &[Node<'_>], from: usize) -> Option<usize> {
let quote = |leaf: &Node<'_>| leaf.kind() == "dollar_quote" && !leaf.byte_range().is_empty();
let open = from + leaves[from..].iter().position(quote)?;
let tag = leaves[open].byte_range().len();
let close = leaves[open + 1..]
.iter()
.position(|leaf| quote(leaf) && leaf.byte_range().len() == tag)?;
Some(open + 1 + close)
}
fn terminated(leaves: &[Node<'_>], index: usize) -> usize {
match leaves.get(index + 1) {
Some(next) if next.kind() == ";" => next.end_byte(),
_ => leaves[index].end_byte(),
}
}
fn leaves<'tree>(root: &Node<'tree>) -> impl Iterator<Item = Node<'tree>> {
let mut pending: Vec<Node<'tree>> = vec![*root];
std::iter::from_fn(move || {
while let Some(node) = pending.pop() {
if node.child_count() == 0 {
return Some(node);
}
let mut cursor = node.walk();
let children: Vec<Node<'tree>> = node.children(&mut cursor).collect();
pending.extend(children.into_iter().rev());
}
None
})
}
fn item(head: Head, start_node: &Node<'_>, end: usize, source: &str) -> StructureItem {
let start = start_node.start_position();
StructureItem {
kind: head.kind,
name: Some(head.name),
span: span_between_from(source, start_node.start_byte(), end, (start.row, start.column)),
..StructureItem::default()
}
}