use async_lsp::lsp_types::Location;
use ropey::Rope;
use wcl_lang::eval::{ScopeId, ScopeKind};
use wcl_lang::lang::ast::*;
use crate::ast_utils::{find_node_at_offset, NodeAtOffset};
use crate::convert::span_to_lsp_range;
use crate::state::AnalysisResult;
pub fn find_references(
analysis: &AnalysisResult,
offset: usize,
rope: &Rope,
uri: &async_lsp::lsp_types::Url,
include_declaration: bool,
) -> Vec<Location> {
let node = find_node_at_offset(&analysis.ast, offset);
if let NodeAtOffset::BlockKind(block) = &node {
let kind_name = &block.kind.name;
let mut locations = Vec::new();
collect_block_kinds(&analysis.ast, kind_name, uri, rope, &mut locations);
return locations;
}
if let NodeAtOffset::SchemaName(schema) = &node {
let schema_name = wcl_lang::schema::schema::string_lit_to_string(&schema.name);
let mut locations = Vec::new();
if include_declaration {
locations.push(Location {
uri: uri.clone(),
range: span_to_lsp_range(schema.name.span, rope),
});
}
collect_block_kinds(&analysis.ast, &schema_name, uri, rope, &mut locations);
return locations;
}
let target_name = match &node {
NodeAtOffset::IdentRef(ident) => &ident.name,
NodeAtOffset::AttributeName(attr) => &attr.name.name,
NodeAtOffset::LetBindingName(lb) => &lb.name.name,
NodeAtOffset::MacroDefName(md) => &md.name.name,
NodeAtOffset::MacroCallName(mc) => &mc.name.name,
_ => return Vec::new(),
};
let mut locations = Vec::new();
collect_name_refs(&analysis.ast, target_name, uri, rope, &mut locations);
if let Some(constraint) = find_scope_constraint(analysis, target_name, offset) {
locations.retain(|loc| {
let constraint_range = span_to_lsp_range(constraint, rope);
loc.range.start >= constraint_range.start && loc.range.end <= constraint_range.end
});
}
if !include_declaration {
if let Some(def_span) = find_def_span(analysis, target_name) {
let def_range = span_to_lsp_range(def_span, rope);
locations.retain(|loc| loc.range != def_range);
}
}
locations
}
fn find_scope_constraint(
analysis: &AnalysisResult,
name: &str,
offset: usize,
) -> Option<wcl_lang::lang::span::Span> {
let def_scope_id = find_def_scope_for_offset(analysis, name, offset)?;
let scope = analysis.scopes.get(def_scope_id);
if scope.kind == ScopeKind::Module {
return None;
}
let entry = scope.entries.get(name)?;
let def_start = entry.span.start;
find_enclosing_block_span(&analysis.ast, def_start)
}
fn find_def_scope_for_offset(
analysis: &AnalysisResult,
name: &str,
offset: usize,
) -> Option<ScopeId> {
let mut best: Option<(ScopeId, usize)> = None;
for scope in analysis.scopes.all_scopes() {
if let Some(entry) = scope.entries.get(name) {
if entry.span.start == 0 && entry.span.end == 0 {
continue;
}
if offset >= entry.span.start && offset < entry.span.end {
return Some(scope.id);
}
let dist = if offset < entry.span.start {
entry.span.start - offset
} else {
offset - entry.span.end
};
if best.is_none() || dist < best.unwrap().1 {
best = Some((scope.id, dist));
}
}
}
best.map(|(id, _)| id)
}
fn find_enclosing_block_span(doc: &Document, offset: usize) -> Option<wcl_lang::lang::span::Span> {
let mut result = None;
for item in &doc.items {
if let DocItem::Body(body_item) = item {
find_enclosing_block_in_body(body_item, offset, &mut result);
}
}
result
}
fn find_enclosing_block_in_body(
item: &BodyItem,
offset: usize,
result: &mut Option<wcl_lang::lang::span::Span>,
) {
match item {
BodyItem::Block(block) => {
if offset >= block.span.start && offset < block.span.end {
match result {
Some(existing) => {
let existing_size = existing.end - existing.start;
let new_size = block.span.end - block.span.start;
if new_size < existing_size {
*result = Some(block.span);
}
}
None => *result = Some(block.span),
}
for child in &block.body {
find_enclosing_block_in_body(child, offset, result);
}
}
}
BodyItem::ForLoop(fl) => {
if offset >= fl.span.start && offset < fl.span.end {
for child in &fl.body {
find_enclosing_block_in_body(child, offset, result);
}
}
}
BodyItem::Conditional(cond) => {
for child in &cond.then_body {
find_enclosing_block_in_body(child, offset, result);
}
if let Some(else_branch) = &cond.else_branch {
match else_branch {
ElseBranch::ElseIf(inner) => {
find_enclosing_block_in_body(
&BodyItem::Conditional(inner.as_ref().clone()),
offset,
result,
);
}
ElseBranch::Else(body, _, _) => {
for child in body {
find_enclosing_block_in_body(child, offset, result);
}
}
}
}
}
_ => {}
}
}
fn find_def_span(analysis: &AnalysisResult, name: &str) -> Option<wcl_lang::lang::span::Span> {
for scope in analysis.scopes.all_scopes() {
if let Some(entry) = scope.entries.get(name) {
if entry.span.start != 0 || entry.span.end != 0 {
return Some(entry.span);
}
}
}
None
}
fn collect_block_kinds(
doc: &Document,
kind_name: &str,
uri: &async_lsp::lsp_types::Url,
rope: &Rope,
out: &mut Vec<Location>,
) {
for item in &doc.items {
if let DocItem::Body(body_item) = item {
collect_block_kinds_in_body(body_item, kind_name, uri, rope, out);
}
}
}
fn collect_block_kinds_in_body(
item: &BodyItem,
kind_name: &str,
uri: &async_lsp::lsp_types::Url,
rope: &Rope,
out: &mut Vec<Location>,
) {
match item {
BodyItem::Block(block) => {
if block.kind.name == kind_name {
out.push(Location {
uri: uri.clone(),
range: span_to_lsp_range(block.kind.span, rope),
});
}
for child in &block.body {
collect_block_kinds_in_body(child, kind_name, uri, rope, out);
}
}
BodyItem::ForLoop(fl) => {
for child in &fl.body {
collect_block_kinds_in_body(child, kind_name, uri, rope, out);
}
}
BodyItem::Conditional(cond) => {
for child in &cond.then_body {
collect_block_kinds_in_body(child, kind_name, uri, rope, out);
}
if let Some(else_branch) = &cond.else_branch {
match else_branch {
ElseBranch::ElseIf(inner) => {
collect_block_kinds_in_body(
&BodyItem::Conditional(inner.as_ref().clone()),
kind_name,
uri,
rope,
out,
);
}
ElseBranch::Else(body, _, _) => {
for child in body {
collect_block_kinds_in_body(child, kind_name, uri, rope, out);
}
}
}
}
}
_ => {}
}
}
fn collect_name_refs(
doc: &Document,
name: &str,
uri: &async_lsp::lsp_types::Url,
rope: &Rope,
out: &mut Vec<Location>,
) {
for item in &doc.items {
match item {
DocItem::Body(body_item) => collect_in_body(body_item, name, uri, rope, out),
DocItem::ExportLet(el) => {
if el.name.name == name {
out.push(Location {
uri: uri.clone(),
range: span_to_lsp_range(el.name.span, rope),
});
}
collect_in_expr(&el.value, name, uri, rope, out);
}
DocItem::ExportMacro(m) => {
if m.name.name == name {
out.push(Location {
uri: uri.clone(),
range: span_to_lsp_range(m.name.span, rope),
});
}
}
DocItem::ReExport(re) => {
if re.name.name == name {
out.push(Location {
uri: uri.clone(),
range: span_to_lsp_range(re.name.span, rope),
});
}
}
DocItem::Import(_) | DocItem::FunctionDecl(_) => {}
DocItem::Namespace(ns) => {
for inner in &ns.items {
collect_in_doc_item(inner, name, uri, rope, out);
}
}
DocItem::Use(_) => {}
}
}
}
fn collect_in_doc_item(
item: &DocItem,
name: &str,
uri: &async_lsp::lsp_types::Url,
rope: &Rope,
out: &mut Vec<Location>,
) {
match item {
DocItem::Body(body_item) => collect_in_body(body_item, name, uri, rope, out),
DocItem::ExportLet(el) => {
if el.name.name == name {
out.push(Location {
uri: uri.clone(),
range: span_to_lsp_range(el.name.span, rope),
});
}
collect_in_expr(&el.value, name, uri, rope, out);
}
DocItem::ExportMacro(m) => {
if m.name.name == name {
out.push(Location {
uri: uri.clone(),
range: span_to_lsp_range(m.name.span, rope),
});
}
}
DocItem::ReExport(re) => {
if re.name.name == name {
out.push(Location {
uri: uri.clone(),
range: span_to_lsp_range(re.name.span, rope),
});
}
}
DocItem::Import(_) | DocItem::FunctionDecl(_) => {}
DocItem::Namespace(ns) => {
for inner in &ns.items {
collect_in_doc_item(inner, name, uri, rope, out);
}
}
DocItem::Use(_) => {}
}
}
fn collect_in_body(
item: &BodyItem,
name: &str,
uri: &async_lsp::lsp_types::Url,
rope: &Rope,
out: &mut Vec<Location>,
) {
match item {
BodyItem::Attribute(attr) => {
if attr.name.name == name {
out.push(Location {
uri: uri.clone(),
range: span_to_lsp_range(attr.name.span, rope),
});
}
collect_in_expr(&attr.value, name, uri, rope, out);
}
BodyItem::Block(block) => {
if block.kind.name == name {
out.push(Location {
uri: uri.clone(),
range: span_to_lsp_range(block.kind.span, rope),
});
}
for child in &block.body {
collect_in_body(child, name, uri, rope, out);
}
}
BodyItem::LetBinding(lb) => {
if lb.name.name == name {
out.push(Location {
uri: uri.clone(),
range: span_to_lsp_range(lb.name.span, rope),
});
}
collect_in_expr(&lb.value, name, uri, rope, out);
}
BodyItem::MacroDef(md) => {
if md.name.name == name {
out.push(Location {
uri: uri.clone(),
range: span_to_lsp_range(md.name.span, rope),
});
}
}
BodyItem::MacroCall(mc) => {
if mc.name.name == name {
out.push(Location {
uri: uri.clone(),
range: span_to_lsp_range(mc.name.span, rope),
});
}
}
BodyItem::ForLoop(fl) => {
if fl.iterator.name == name {
out.push(Location {
uri: uri.clone(),
range: span_to_lsp_range(fl.iterator.span, rope),
});
}
collect_in_expr(&fl.iterable, name, uri, rope, out);
for child in &fl.body {
collect_in_body(child, name, uri, rope, out);
}
}
BodyItem::Conditional(cond) => {
collect_in_expr(&cond.condition, name, uri, rope, out);
for child in &cond.then_body {
collect_in_body(child, name, uri, rope, out);
}
if let Some(else_branch) = &cond.else_branch {
match else_branch {
ElseBranch::ElseIf(inner) => {
collect_in_body(
&BodyItem::Conditional(inner.as_ref().clone()),
name,
uri,
rope,
out,
);
}
ElseBranch::Else(body, _, _) => {
for child in body {
collect_in_body(child, name, uri, rope, out);
}
}
}
}
}
BodyItem::Validation(val) => {
collect_in_expr(&val.check, name, uri, rope, out);
collect_in_expr(&val.message, name, uri, rope, out);
}
BodyItem::Table(_)
| BodyItem::Schema(_)
| BodyItem::DecoratorSchema(_)
| BodyItem::SymbolSetDecl(_)
| BodyItem::StructDef(_) => {}
}
}
fn collect_in_expr(
expr: &Expr,
name: &str,
uri: &async_lsp::lsp_types::Url,
rope: &Rope,
out: &mut Vec<Location>,
) {
match expr {
Expr::Ident(ident) => {
if ident.name == name {
out.push(Location {
uri: uri.clone(),
range: span_to_lsp_range(ident.span, rope),
});
}
}
Expr::BinaryOp(lhs, _, rhs, _) => {
collect_in_expr(lhs, name, uri, rope, out);
collect_in_expr(rhs, name, uri, rope, out);
}
Expr::UnaryOp(_, inner, _) | Expr::Paren(inner, _) => {
collect_in_expr(inner, name, uri, rope, out);
}
Expr::Ternary(a, b, c, _) => {
collect_in_expr(a, name, uri, rope, out);
collect_in_expr(b, name, uri, rope, out);
collect_in_expr(c, name, uri, rope, out);
}
Expr::MemberAccess(obj, _, _) => {
collect_in_expr(obj, name, uri, rope, out);
}
Expr::IndexAccess(obj, idx, _) => {
collect_in_expr(obj, name, uri, rope, out);
collect_in_expr(idx, name, uri, rope, out);
}
Expr::FnCall(callee, args, _) => {
collect_in_expr(callee, name, uri, rope, out);
for arg in args {
let e = match arg {
CallArg::Positional(e) => e,
CallArg::Named(_, e) => e,
};
collect_in_expr(e, name, uri, rope, out);
}
}
Expr::List(items, _) => {
for item in items {
collect_in_expr(item, name, uri, rope, out);
}
}
Expr::Map(entries, _) => {
for (_, val) in entries {
collect_in_expr(val, name, uri, rope, out);
}
}
Expr::Lambda(_, body, _) => collect_in_expr(body, name, uri, rope, out),
Expr::BlockExpr(_, final_expr, _) => collect_in_expr(final_expr, name, uri, rope, out),
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::analyze;
use async_lsp::lsp_types::Url;
fn get_refs(source: &str, offset: usize, include_decl: bool) -> Vec<Location> {
let analysis = analyze(source, &wcl_lang::ParseOptions::default());
let rope = Rope::from_str(source);
let uri = Url::parse("file:///test.wcl").unwrap();
find_references(&analysis, offset, &rope, &uri, include_decl)
}
#[test]
fn test_find_refs_of_let_binding() {
let source = "let x = 42\nconfig { port = x }";
let offset = source.find("x").unwrap();
let refs = get_refs(source, offset, true);
assert!(refs.len() >= 2, "expected >= 2 refs, got {}", refs.len());
}
#[test]
fn test_find_refs_include_declaration_has_more() {
let source = "let x = 42\nconfig { port = x }";
let offset = source.find("x").unwrap();
let refs_with = get_refs(source, offset, true);
assert!(!refs_with.is_empty());
}
#[test]
fn test_find_refs_attribute() {
let source = "config { port = 8080 }";
let offset = source.find("port").unwrap();
let refs = get_refs(source, offset, true);
assert!(!refs.is_empty());
}
#[test]
fn test_find_refs_no_match() {
let source = "config { port = 8080 }";
let offset = source.find("8080").unwrap();
let refs = get_refs(source, offset, true);
assert!(refs.is_empty());
}
#[test]
fn test_find_refs_scoped_variable() {
let source = "server { port = 8080 }\ncache { port = 6379 }";
let offset = source.find("port").unwrap();
let refs = get_refs(source, offset, true);
assert_eq!(
refs.len(),
1,
"expected 1 scoped ref for port in server block, got {}",
refs.len()
);
}
#[test]
fn test_find_refs_block_kind() {
let source = "server web { port = 8080 }\nserver api { port = 9090 }";
let offset = source.find("server").unwrap();
let refs = get_refs(source, offset, true);
assert_eq!(
refs.len(),
2,
"expected 2 block kind refs for 'server', got {}",
refs.len()
);
}
#[test]
fn test_find_refs_module_level_let() {
let source = "let base = 1000\nserver { port = base }\ncache { port = base }";
let offset = source.find("base").unwrap();
let refs = get_refs(source, offset, true);
assert!(
refs.len() >= 3,
"expected >= 3 refs for module-level 'base', got {}",
refs.len()
);
}
#[test]
fn test_find_refs_schema_name() {
let source =
"schema \"server\" {\n port: i64\n}\nserver web { port = 8080 }\nserver api { port = 9090 }";
let offset = source.find("\"server\"").unwrap() + 1;
let refs = get_refs(source, offset, true);
assert_eq!(
refs.len(),
3,
"expected 3 refs for schema 'server', got {}",
refs.len()
);
}
#[test]
fn test_find_refs_schema_excludes_declaration() {
let source = "schema \"server\" {\n port: i64\n}\nserver web { port = 8080 }";
let offset = source.find("\"server\"").unwrap() + 1;
let refs = get_refs(source, offset, false);
assert_eq!(
refs.len(),
1,
"expected 1 ref (excluding declaration), got {}",
refs.len()
);
}
}