use crate::Id;
use nu_protocol::{
DeclId, ModuleId, Span,
ast::{Argument, Block, Call, Expr, Expression, FindMapResult, ListItem, PathMember, Traverse},
engine::StateWorkingSet,
};
use std::sync::Arc;
fn strip_quotes(span: Span, working_set: &StateWorkingSet) -> (Box<[u8]>, Span) {
let text = working_set.get_span_contents(span);
if text.len() > 1
&& ((text.starts_with(b"\"") && text.ends_with(b"\""))
|| (text.starts_with(b"'") && text.ends_with(b"'")))
{
(
text.get(1..text.len() - 1)
.expect("Invalid quoted span!")
.into(),
Span::new(span.start.saturating_add(1), span.end.saturating_sub(1)),
)
} else {
(text.into(), span)
}
}
fn strip_dollar_sign(span: Span, working_set: &StateWorkingSet<'_>) -> (Box<[u8]>, Span) {
let content = working_set.get_span_contents(span);
if content.starts_with(b"$") {
(
content[1..].into(),
Span::new(span.start.saturating_add(1), span.end),
)
} else {
(content.into(), span)
}
}
fn command_name_span_from_call_head(
working_set: &StateWorkingSet,
decl_id: DeclId,
head_span: Span,
) -> Span {
let name = working_set.get_decl(decl_id).name();
if name.len() == head_span.end.saturating_sub(head_span.start) {
return head_span;
}
let head_content = working_set.get_span_contents(head_span);
let mut head_words = head_content.split(|c| *c == b' ').collect::<Vec<_>>();
let mut name_words = name.split(' ').collect::<Vec<_>>();
let mut matched_len = name_words.len() - 1;
while let Some(name_word) = name_words.pop() {
while let Some(head_word) = head_words.pop() {
if head_word.is_empty() && !name_word.is_empty() {
matched_len += 1;
continue;
}
if name_word.as_bytes() == head_word {
matched_len += head_word.len();
break;
} else {
return head_span;
}
}
if name_words.len() > head_words.len() {
return head_span;
}
}
Span::new(head_span.end.saturating_sub(matched_len), head_span.end)
}
fn try_find_id_in_misc(
call: &Call,
working_set: &StateWorkingSet,
location: Option<&usize>,
id_ref: Option<&Id>,
) -> Option<(Id, Span)> {
let call_name = working_set.get_decl(call.decl_id).name();
match call_name {
"def" | "export def" => try_find_id_in_def(call, working_set, location, id_ref),
"module" | "export module" => try_find_id_in_mod(call, working_set, location, id_ref),
"use" | "export use" | "hide" => try_find_id_in_use(call, working_set, location, id_ref),
"overlay use" | "overlay hide" => {
try_find_id_in_overlay(call, working_set, location, id_ref)
}
_ => None,
}
}
fn try_find_id_in_def(
call: &Call,
working_set: &StateWorkingSet,
location: Option<&usize>,
id_ref: Option<&Id>,
) -> Option<(Id, Span)> {
if let Some(id_ref) = id_ref
&& !matches!(id_ref, Id::Declaration(_))
{
return None;
}
let mut span = None;
for arg in call.arguments.iter() {
if location.is_none_or(|pos| arg.span().contains(*pos)) {
if let Argument::Positional(expr) = arg
&& let Expr::String(_) = &expr.expr
{
span = Some(expr.span);
break;
}
if location.is_some() {
return None;
}
}
}
let block_span_of_this_def = call.positional_iter().last()?.span;
let decl_on_spot = |decl_id: &DeclId| -> bool {
working_set
.get_decl(*decl_id)
.block_id()
.and_then(|block_id| working_set.get_block(block_id).span)
.is_some_and(|block_span| block_span == block_span_of_this_def)
};
let (_, span) = strip_quotes(span?, working_set);
let id_found = if let Some(id_r) = id_ref {
let Id::Declaration(decl_id_ref) = id_r else {
return None;
};
decl_on_spot(decl_id_ref).then_some(id_r.clone())?
} else {
Id::Declaration((0..working_set.num_decls()).rev().find_map(|id| {
let decl_id = DeclId::new(id);
decl_on_spot(&decl_id).then_some(decl_id)
})?)
};
Some((id_found, span))
}
fn try_find_id_in_mod(
call: &Call,
working_set: &StateWorkingSet,
location: Option<&usize>,
id_ref: Option<&Id>,
) -> Option<(Id, Span)> {
if let Some(id_ref) = id_ref
&& !matches!(id_ref, Id::Module(_, _))
{
return None;
}
let check_location = |span: &Span| location.is_none_or(|pos| span.contains(*pos));
call.arguments.first().and_then(|arg| {
if !check_location(&arg.span()) {
return None;
}
match arg {
Argument::Positional(expr) => {
let name = expr.as_string()?;
let module_id = working_set.find_module(name.as_bytes()).or_else(|| {
let mut any_id = true;
let mut id_num_ref = 0;
if let Some(Id::Module(id_ref, _)) = id_ref {
any_id = false;
id_num_ref = id_ref.get();
}
let block_span = call.arguments.last()?.span();
(0..working_set.num_modules())
.rfind(|id| {
(any_id || id_num_ref == *id)
&& working_set.get_module(ModuleId::new(*id)).span.is_some_and(
|mod_span| {
mod_span.start <= block_span.start + 1
&& block_span.start <= mod_span.start
&& block_span.end >= mod_span.end
&& block_span.end <= mod_span.end + 1
},
)
})
.map(ModuleId::new)
})?;
let found_id = Id::Module(module_id, name.as_bytes().into());
let found_span = strip_quotes(arg.span(), working_set).1;
id_ref
.is_none_or(|id_r| found_id == *id_r)
.then_some((found_id, found_span))
}
_ => None,
}
})
}
fn try_find_id_in_use(
call: &Call,
working_set: &StateWorkingSet,
location: Option<&usize>,
id_ref: Option<&Id>,
) -> Option<(Id, Span)> {
let Expression {
expr: Expr::ImportPattern(import_pattern),
..
} = call.get_parser_info("import_pattern")?
else {
return None;
};
let module_id = import_pattern.head.id?;
let find_by_name = |name: &[u8]| {
let module = working_set.get_module(module_id);
match id_ref {
Some(Id::Variable(var_id_ref, name_ref)) => module
.constants
.get(name)
.cloned()
.or_else(|| {
(name_ref.as_ref() == name
&& call
.head
.contains_span(working_set.get_variable(*var_id_ref).declaration_span))
.then_some(*var_id_ref)
})
.and_then(|var_id| {
(var_id == *var_id_ref).then_some(Id::Variable(var_id, name.into()))
}),
Some(Id::Declaration(decl_id_ref)) => module.decls.get(name).and_then(|decl_id| {
(*decl_id == *decl_id_ref).then_some(Id::Declaration(*decl_id))
}),
Some(Id::Module(module_id_ref, name_ref)) => {
module.submodules.get(name).and_then(|module_id| {
(*module_id == *module_id_ref && name_ref.as_ref() == name)
.then_some(Id::Module(*module_id, name.into()))
})
}
None => module
.submodules
.get(name)
.map(|id| Id::Module(*id, name.into()))
.or(module.decls.get(name).cloned().map(Id::Declaration))
.or(module
.constants
.get(name)
.map(|id| Id::Variable(*id, name.into()))),
_ => None,
}
};
let check_location = |span: &Span| location.is_none_or(|pos| span.contains(*pos));
let module_name = call.arguments.first()?;
let span = module_name.span();
let (span_content, clean_span) = strip_quotes(span, working_set);
if let Some(Id::Module(id_ref, name_ref)) = id_ref {
if module_id == *id_ref && name_ref == &span_content {
return Some((Id::Module(module_id, span_content), clean_span));
}
}
if let Some(pos) = location {
if span.contains(*pos) {
return Some((Id::Module(module_id, span_content), clean_span));
}
}
let search_in_list_items = |items: &Vec<ListItem>| {
items.iter().find_map(|item| {
let item_expr = item.expr();
check_location(&item_expr.span)
.then_some(item_expr)
.and_then(|e| {
let name = e.as_string()?;
Some((
find_by_name(name.as_bytes())?,
strip_quotes(item_expr.span, working_set).1,
))
})
})
};
for arg in call.arguments.get(1..)?.iter().rev() {
let Argument::Positional(expr) = arg else {
continue;
};
if !check_location(&expr.span) {
continue;
}
let matched = match &expr.expr {
Expr::String(name) => {
find_by_name(name.as_bytes()).map(|id| (id, strip_quotes(expr.span, working_set).1))
}
Expr::List(items) => search_in_list_items(items),
Expr::FullCellPath(fcp) => {
let Expr::List(items) = &fcp.head.expr else {
return None;
};
search_in_list_items(items)
}
_ => None,
};
if matched.is_some() || location.is_some() {
return matched;
}
}
None
}
fn try_find_id_in_overlay(
call: &Call,
working_set: &StateWorkingSet,
location: Option<&usize>,
id_ref: Option<&Id>,
) -> Option<(Id, Span)> {
if let Some(id_ref) = id_ref
&& !matches!(id_ref, Id::Module(_, _))
{
return None;
}
let check_location = |span: &Span| location.is_none_or(|pos| span.contains(*pos));
let module_from_parser_info = |span: Span, name: &str| {
let Expression {
expr: Expr::Overlay(Some(module_id)),
..
} = call.get_parser_info("overlay_expr")?
else {
return None;
};
let found_id = Id::Module(*module_id, name.as_bytes().into());
id_ref
.is_none_or(|id_r| found_id == *id_r)
.then_some((found_id, strip_quotes(span, working_set).1))
};
let module_from_overlay_name = |name: &str, span: Span| {
let found_id = Id::Module(
working_set.find_overlay(name.as_bytes())?.origin,
name.as_bytes().into(),
);
id_ref
.is_none_or(|id_r| found_id == *id_r)
.then_some((found_id, strip_quotes(span, working_set).1))
};
for arg in call.arguments.iter().rev() {
let Argument::Positional(expr) = arg else {
continue;
};
if !check_location(&expr.span) {
continue;
};
let matched = match &expr.expr {
Expr::String(name) => module_from_parser_info(expr.span, name)
.or_else(|| module_from_overlay_name(name, expr.span)),
Expr::Keyword(kwd) => match &kwd.expr.expr {
Expr::String(name) => module_from_parser_info(kwd.expr.span, name)
.or_else(|| module_from_overlay_name(name, kwd.expr.span)),
_ => None,
},
_ => None,
};
if matched.is_some() || location.is_some() {
return matched;
}
}
None
}
fn find_id_in_expr(
expr: &Expression,
working_set: &StateWorkingSet,
location: &usize,
) -> FindMapResult<(Id, Span)> {
if !expr.span.contains(*location) {
return FindMapResult::Stop;
}
let span = expr.span;
match &expr.expr {
Expr::VarDecl(var_id) | Expr::Var(var_id) => {
let (name, clean_span) = strip_dollar_sign(span, working_set);
FindMapResult::Found((Id::Variable(*var_id, name), clean_span))
}
Expr::Call(call) => {
if call.head.contains(*location) {
let span = command_name_span_from_call_head(working_set, call.decl_id, call.head);
FindMapResult::Found((Id::Declaration(call.decl_id), span))
} else {
try_find_id_in_misc(call, working_set, Some(location), None)
.map(FindMapResult::Found)
.unwrap_or_default()
}
}
Expr::ExternalCall(head, _) => {
if head.span.contains(*location)
&& let Expr::GlobPattern(cmd, _) = &head.expr
{
return FindMapResult::Found((Id::External(cmd.clone()), head.span));
}
FindMapResult::Continue
}
Expr::FullCellPath(fcp) => {
if fcp.head.span.contains(*location) {
FindMapResult::Continue
} else {
let Expression {
expr: Expr::Var(var_id),
..
} = fcp.head
else {
return FindMapResult::Continue;
};
let tail: Vec<PathMember> = fcp
.tail
.clone()
.into_iter()
.take_while(|pm| pm.span().start <= *location)
.collect();
let Some(span) = tail.last().map(|pm| pm.span()) else {
return FindMapResult::Stop;
};
FindMapResult::Found((Id::CellPath(var_id, tail), span))
}
}
Expr::Overlay(Some(module_id)) => {
FindMapResult::Found((Id::Module(*module_id, [].into()), span))
}
Expr::Bool(_)
| Expr::Binary(_)
| Expr::DateTime(_)
| Expr::Directory(_, _)
| Expr::Filepath(_, _)
| Expr::Float(_)
| Expr::Garbage
| Expr::GlobPattern(_, _)
| Expr::Int(_)
| Expr::Nothing
| Expr::RawString(_)
| Expr::Signature(_)
| Expr::String(_) => FindMapResult::Found((Id::Value(expr.ty.clone()), span)),
_ => FindMapResult::Continue,
}
}
pub(crate) fn find_id(
ast: &Arc<Block>,
working_set: &StateWorkingSet,
location: &usize,
) -> Option<(Id, Span)> {
let closure = |e| find_id_in_expr(e, working_set, location);
ast.find_map(working_set, &closure)
}
fn find_reference_by_id_in_expr(
expr: &Expression,
working_set: &StateWorkingSet,
id: &Id,
) -> Vec<Span> {
match (&expr.expr, id) {
(Expr::Var(vid1), Id::Variable(vid2, _)) if *vid1 == *vid2 => vec![Span::new(
expr.span.start.saturating_add(1),
expr.span.end,
)],
(Expr::VarDecl(vid1), Id::Variable(vid2, _)) if *vid1 == *vid2 => vec![expr.span],
(Expr::Call(call), _) => match id {
Id::Declaration(decl_id) if call.decl_id == *decl_id => {
vec![command_name_span_from_call_head(
working_set,
call.decl_id,
call.head,
)]
}
_ => try_find_id_in_misc(call, working_set, None, Some(id))
.map(|(_, span_found)| span_found)
.into_iter()
.collect::<Vec<_>>(),
},
_ => vec![],
}
}
pub(crate) fn find_reference_by_id(
ast: &Arc<Block>,
working_set: &StateWorkingSet,
id: &Id,
) -> Vec<Span> {
let mut results = Vec::new();
let closure = |e| find_reference_by_id_in_expr(e, working_set, id);
ast.flat_map(working_set, &closure, &mut results);
results
}