mod defs;
mod exprs;
mod patterns;
#[cfg(test)]
mod tests;
use super::position::span_contains_offset;
use crate::ast::{
EnumDef, EnumVariant, Expr, FieldDef, File, FnDef, FnParam, FunctionDef, Ident, ImplDef,
LetBinding, ModuleDef, Statement, StructDef, StructField, TraitDef, Type, UseItems, UseStmt,
};
#[expect(
clippy::exhaustive_enums,
reason = "matched exhaustively by consumer code"
)]
#[derive(Debug, Clone)]
pub enum NodeAtPosition<'ast> {
File,
UseStatement(&'ast UseStmt),
LetBinding(&'ast LetBinding),
TraitDef(&'ast TraitDef),
StructDef(&'ast StructDef),
EnumDef(&'ast EnumDef),
EnumVariant(&'ast EnumVariant),
FieldDef(&'ast FieldDef),
StructField(&'ast StructField),
MountField(&'ast StructField),
Type(&'ast Type),
Expression(&'ast Expr),
Identifier(&'ast Ident),
ImplDef(&'ast ImplDef),
ModuleDef(&'ast ModuleDef),
FunctionDef(&'ast FunctionDef),
FnDef(&'ast FnDef),
FunctionParam(&'ast FnParam),
None,
}
#[expect(clippy::exhaustive_structs, reason = "public API type")]
#[derive(Debug, Clone)]
pub struct PositionContext<'ast> {
pub node: NodeAtPosition<'ast>,
pub parents: Vec<NodeAtPosition<'ast>>,
pub offset: usize,
}
impl<'ast> PositionContext<'ast> {
#[must_use]
pub fn enclosing_definition(&self) -> Option<&NodeAtPosition<'ast>> {
self.parents.iter().find(|node| {
matches!(
node,
NodeAtPosition::TraitDef(_)
| NodeAtPosition::StructDef(_)
| NodeAtPosition::EnumDef(_)
| NodeAtPosition::ImplDef(_)
| NodeAtPosition::ModuleDef(_)
| NodeAtPosition::FunctionDef(_)
| NodeAtPosition::FnDef(_)
)
})
}
#[must_use]
pub fn is_in_expression(&self) -> bool {
matches!(self.node, NodeAtPosition::Expression(_))
|| self
.parents
.iter()
.any(|n| matches!(n, NodeAtPosition::Expression(_)))
}
#[must_use]
pub const fn is_in_type_position(&self) -> bool {
matches!(self.node, NodeAtPosition::Type(_))
}
}
#[must_use]
pub fn find_node_at_offset(file: &File, offset: usize) -> PositionContext<'_> {
let mut finder = NodeFinder {
offset,
parents: Vec::new(),
found_node: None,
};
finder.visit_file(file);
PositionContext {
node: finder.found_node.unwrap_or(NodeAtPosition::File),
parents: finder.parents,
offset,
}
}
struct NodeFinder<'ast> {
offset: usize,
parents: Vec<NodeAtPosition<'ast>>,
found_node: Option<NodeAtPosition<'ast>>,
}
impl<'ast> NodeFinder<'ast> {
fn visit_file(&mut self, file: &'ast File) {
if !span_contains_offset(&file.span, self.offset) {
return;
}
for statement in &file.statements {
self.visit_statement(statement);
if self.found_node.is_some() {
return;
}
}
}
fn visit_statement(&mut self, statement: &'ast Statement) {
match statement {
Statement::Use(use_stmt) => {
if span_contains_offset(&use_stmt.span, self.offset) {
self.found_node = Some(NodeAtPosition::UseStatement(use_stmt));
self.visit_use_stmt(use_stmt);
}
}
Statement::Let(let_binding) => {
if span_contains_offset(&let_binding.span, self.offset) {
self.parents.push(NodeAtPosition::LetBinding(let_binding));
self.visit_let_binding(let_binding);
if self.found_node.is_none() {
self.parents.pop();
}
}
}
Statement::Definition(definition) => {
self.visit_definition(definition.as_ref());
}
}
}
fn visit_use_stmt(&mut self, use_stmt: &'ast UseStmt) {
for ident in &use_stmt.path {
if span_contains_offset(&ident.span, self.offset) {
self.found_node = Some(NodeAtPosition::Identifier(ident));
return;
}
}
match &use_stmt.items {
UseItems::Single(ident) => {
if span_contains_offset(&ident.span, self.offset) {
self.found_node = Some(NodeAtPosition::Identifier(ident));
}
}
UseItems::Multiple(idents) => {
for ident in idents {
if span_contains_offset(&ident.span, self.offset) {
self.found_node = Some(NodeAtPosition::Identifier(ident));
return;
}
}
}
UseItems::Glob => {} }
}
fn visit_let_binding(&mut self, let_binding: &'ast LetBinding) {
self.visit_binding_pattern(&let_binding.pattern);
if self.found_node.is_some() {
return;
}
self.visit_expr(&let_binding.value);
}
}