use cairo_lang_defs::db::DefsGroup;
use cairo_lang_defs::ids::{
LanguageElementId, LookupItemId, ModuleFileId, ModuleId, NamedLanguageElementId,
TopLevelLanguageElementId, TraitFunctionId,
};
use cairo_lang_filesystem::db::FilesGroup;
use cairo_lang_filesystem::ids::FileId;
use cairo_lang_filesystem::span::TextOffset;
use cairo_lang_semantic::corelib::{core_submodule, get_submodule};
use cairo_lang_semantic::db::SemanticGroup;
use cairo_lang_semantic::diagnostic::{NotFoundItemType, SemanticDiagnostics};
use cairo_lang_semantic::expr::inference::InferenceId;
use cairo_lang_semantic::items::function_with_body::SemanticExprLookup;
use cairo_lang_semantic::items::us::SemanticUseEx;
use cairo_lang_semantic::items::visibility::peek_visible_in;
use cairo_lang_semantic::lookup_item::{HasResolverData, LookupItemEx};
use cairo_lang_semantic::resolve::{ResolvedConcreteItem, ResolvedGenericItem, Resolver};
use cairo_lang_semantic::types::peel_snapshots;
use cairo_lang_semantic::{ConcreteTypeId, Pattern, TypeLongId};
use cairo_lang_syntax::node::ast::PathSegment;
use cairo_lang_syntax::node::{TypedStablePtr, TypedSyntaxNode, ast};
use cairo_lang_utils::{LookupIntern, Upcast};
use lsp_types::{CompletionItem, CompletionItemKind, InsertTextFormat, Position, Range, TextEdit};
use tracing::debug;
use crate::lang::db::{AnalysisDatabase, LsSemanticGroup};
use crate::lang::inspect::methods::find_methods_for_type;
use crate::lang::lsp::ToLsp;
pub fn generic_completions(
db: &AnalysisDatabase,
module_file_id: ModuleFileId,
lookup_items: Vec<LookupItemId>,
) -> Vec<CompletionItem> {
let mut completions = vec![];
completions.extend(db.crate_configs().keys().map(|crate_id| CompletionItem {
label: crate_id.lookup_intern(db).name().into(),
kind: Some(CompletionItemKind::MODULE),
..CompletionItem::default()
}));
if let Ok(module_items) = db.module_items(module_file_id.0) {
completions.extend(module_items.iter().map(|item| {
CompletionItem {
label: item.name(db.upcast()).to_string(),
kind: ResolvedGenericItem::from_module_item(db, *item)
.ok()
.map(resolved_generic_item_completion_kind),
..CompletionItem::default()
}
}));
}
let Some(lookup_item_id) = lookup_items.into_iter().next() else {
return completions;
};
let Some(function_id) = lookup_item_id.function_with_body() else {
return completions;
};
let Ok(signature) = db.function_with_body_signature(function_id) else {
return completions;
};
for param in &signature.params {
completions.push(CompletionItem {
label: param.name.clone().into(),
kind: Some(CompletionItemKind::VARIABLE),
..CompletionItem::default()
});
}
let Ok(body) = db.function_body(function_id) else {
return completions;
};
for (_id, pat) in &body.arenas.patterns {
if let Pattern::Variable(var) = pat {
completions.push(CompletionItem {
label: var.name.clone().into(),
kind: Some(CompletionItemKind::VARIABLE),
..CompletionItem::default()
});
}
}
completions
}
fn resolved_generic_item_completion_kind(item: ResolvedGenericItem) -> CompletionItemKind {
match item {
ResolvedGenericItem::GenericConstant(_) => CompletionItemKind::CONSTANT,
ResolvedGenericItem::Module(_) => CompletionItemKind::MODULE,
ResolvedGenericItem::GenericFunction(_) | ResolvedGenericItem::TraitFunction(_) => {
CompletionItemKind::FUNCTION
}
ResolvedGenericItem::GenericType(_) | ResolvedGenericItem::GenericTypeAlias(_) => {
CompletionItemKind::CLASS
}
ResolvedGenericItem::Impl(_) | ResolvedGenericItem::GenericImplAlias(_) => {
CompletionItemKind::CLASS
}
ResolvedGenericItem::Variant(_) => CompletionItemKind::ENUM_MEMBER,
ResolvedGenericItem::Trait(_) => CompletionItemKind::INTERFACE,
ResolvedGenericItem::Variable(_) => CompletionItemKind::VARIABLE,
}
}
pub fn colon_colon_completions(
db: &AnalysisDatabase,
module_file_id: ModuleFileId,
lookup_items: Vec<LookupItemId>,
segments: Vec<PathSegment>,
) -> Option<Vec<CompletionItem>> {
let resolver_data = match lookup_items.into_iter().next() {
Some(item) => {
(*item.resolver_data(db).ok()?).clone_with_inference_id(db, InferenceId::NoContext)
}
None => Resolver::new(db, module_file_id, InferenceId::NoContext).data,
};
let mut resolver = Resolver::with_data(db, resolver_data);
let mut diagnostics = SemanticDiagnostics::default();
let item = resolver
.resolve_concrete_path(&mut diagnostics, segments, NotFoundItemType::Identifier)
.ok()?;
let current_module_id = module_file_id.0;
Some(match item {
ResolvedConcreteItem::Module(module_id) => db
.module_items(module_id)
.ok()?
.iter()
.filter_map(|item| {
let resolved_item = ResolvedGenericItem::from_module_item(db, *item).ok()?;
let item_info = db.module_item_info_by_name(module_id, item.name(db)).ok()??;
peek_visible_in(db, item_info.visibility, module_id, current_module_id).then(|| {
CompletionItem {
label: item.name(db.upcast()).to_string(),
kind: Some(resolved_generic_item_completion_kind(resolved_item)),
..CompletionItem::default()
}
})
})
.collect(),
ResolvedConcreteItem::Trait(item) => db
.trait_functions(item.trait_id(db))
.unwrap_or_default()
.iter()
.map(|(name, _)| CompletionItem {
label: name.to_string(),
kind: Some(CompletionItemKind::FUNCTION),
..CompletionItem::default()
})
.collect(),
ResolvedConcreteItem::Impl(item) => item
.concrete_trait(db)
.map(|trait_id| {
db.trait_functions(trait_id.trait_id(db))
.unwrap_or_default()
.iter()
.map(|(name, _)| CompletionItem {
label: name.to_string(),
kind: Some(CompletionItemKind::FUNCTION),
..CompletionItem::default()
})
.collect()
})
.unwrap_or_default(),
ResolvedConcreteItem::Type(ty) => match ty.lookup_intern(db) {
TypeLongId::Concrete(ConcreteTypeId::Enum(enum_id)) => db
.enum_variants(enum_id.enum_id(db))
.unwrap_or_default()
.iter()
.map(|(name, _)| CompletionItem {
label: name.to_string(),
kind: Some(CompletionItemKind::ENUM_MEMBER),
..CompletionItem::default()
})
.collect(),
_ => vec![],
},
_ => vec![],
})
}
pub fn dot_completions(
db: &AnalysisDatabase,
file_id: FileId,
lookup_items: Vec<LookupItemId>,
expr: ast::ExprBinary,
) -> Option<Vec<CompletionItem>> {
let syntax_db = db.upcast();
let lookup_item_id = lookup_items.into_iter().next()?;
let function_with_body = lookup_item_id.function_with_body()?;
let module_file_id = function_with_body.module_file_id(db.upcast());
let resolver_data = lookup_item_id.resolver_data(db).ok()?;
let resolver = Resolver::with_data(
db,
resolver_data.as_ref().clone_with_inference_id(db, InferenceId::NoContext),
);
let node = expr.lhs(syntax_db);
let stable_ptr = node.stable_ptr().untyped();
let expr_id = db.lookup_expr_by_ptr(function_with_body, node.stable_ptr()).ok()?;
let semantic_expr = db.expr_semantic(function_with_body, expr_id);
let ty = semantic_expr.ty();
if ty.is_missing(db) {
debug!("type is missing");
return None;
}
let offset = if let Some(ModuleId::Submodule(submodule_id)) =
db.find_module_containing_node(&expr.as_syntax_node())
{
let module_def_ast = submodule_id.stable_ptr(db.upcast()).lookup(syntax_db);
if let ast::MaybeModuleBody::Some(body) = module_def_ast.body(syntax_db) {
body.items(syntax_db).as_syntax_node().span_start_without_trivia(syntax_db)
} else {
TextOffset::default()
}
} else {
TextOffset::default()
};
let position = offset.position_in_file(db.upcast(), file_id).unwrap().to_lsp();
let relevant_methods = find_methods_for_type(db, resolver, ty, stable_ptr);
let mut completions = Vec::new();
for trait_function in relevant_methods {
let Some(completion) = completion_for_method(db, module_file_id, trait_function, position)
else {
continue;
};
completions.push(completion);
}
let (_, long_ty) = peel_snapshots(db, ty);
if let TypeLongId::Concrete(ConcreteTypeId::Struct(concrete_struct_id)) = long_ty {
db.concrete_struct_members(concrete_struct_id).ok()?.iter().for_each(|(name, member)| {
let completion = CompletionItem {
label: name.to_string(),
detail: Some(member.ty.format(db.upcast())),
kind: Some(CompletionItemKind::FIELD),
..CompletionItem::default()
};
completions.push(completion);
});
}
Some(completions)
}
pub fn completion_for_method(
db: &AnalysisDatabase,
module_file_id: ModuleFileId,
trait_function: TraitFunctionId,
position: Position,
) -> Option<CompletionItem> {
let trait_id = trait_function.trait_id(db.upcast());
let name = trait_function.name(db.upcast());
db.trait_function_signature(trait_function).ok()?;
let detail = trait_id.full_path(db.upcast());
let mut additional_text_edits = vec![];
if !module_has_trait(db, module_file_id.0, trait_id)? {
if let Some(trait_path) = db.visible_traits_from_module(module_file_id)?.get(&trait_id) {
additional_text_edits.push(TextEdit {
range: Range::new(position, position),
new_text: format!("use {};\n", trait_path),
});
}
}
let completion = CompletionItem {
label: format!("{}()", name),
insert_text: Some(format!("{}($0)", name)),
insert_text_format: Some(InsertTextFormat::SNIPPET),
detail: Some(detail),
kind: Some(CompletionItemKind::METHOD),
additional_text_edits: Some(additional_text_edits),
..CompletionItem::default()
};
Some(completion)
}
fn module_has_trait(
db: &AnalysisDatabase,
module_id: ModuleId,
trait_id: cairo_lang_defs::ids::TraitId,
) -> Option<bool> {
if db.module_traits_ids(module_id).ok()?.contains(&trait_id) {
return Some(true);
}
let mut current_top_module = module_id;
while let ModuleId::Submodule(submodule_id) = current_top_module {
current_top_module = submodule_id.parent_module(db.upcast());
}
let crate_id = match current_top_module {
ModuleId::CrateRoot(crate_id) => crate_id,
ModuleId::Submodule(_) => unreachable!("current module is not a top-level module"),
};
let edition =
db.crate_config(crate_id).map(|config| config.settings.edition).unwrap_or_default();
let prelude_submodule_name = edition.prelude_submodule_name();
let core_prelude_submodule = core_submodule(db, "prelude");
let prelude_submodule = get_submodule(db, core_prelude_submodule, prelude_submodule_name)?;
for module_id in [prelude_submodule, module_id].iter().copied() {
for use_id in db.module_uses_ids(module_id).ok()?.iter().copied() {
if db.use_resolved_item(use_id) == Ok(ResolvedGenericItem::Trait(trait_id)) {
return Some(true);
}
}
}
Some(false)
}
pub fn struct_constructor_completions(
db: &AnalysisDatabase,
lookup_items: Vec<LookupItemId>,
constructor: ast::ExprStructCtorCall,
) -> Option<Vec<CompletionItem>> {
let module_id = db.find_module_containing_node(&constructor.as_syntax_node())?;
let lookup_item_id = lookup_items.into_iter().next()?;
let function_id = lookup_item_id.function_with_body()?;
let already_present_members = constructor
.arguments(db)
.arguments(db)
.elements(db)
.into_iter()
.filter_map(|member| match member {
ast::StructArg::StructArgSingle(struct_arg_single) => {
Some(struct_arg_single.identifier(db).token(db).as_syntax_node().get_text(db))
}
ast::StructArg::StructArgTail(_) => None,
})
.collect::<Vec<_>>();
let constructor_expr_id =
db.lookup_expr_by_ptr(function_id, constructor.stable_ptr().into()).ok()?;
let semantic_expr = db.expr_semantic(function_id, constructor_expr_id);
let cairo_lang_semantic::Expr::StructCtor(constructor_semantic_expr) = semantic_expr else {
return None;
};
let struct_parent_module_id =
constructor_semantic_expr.concrete_struct_id.struct_id(db).parent_module(db);
let struct_members =
db.concrete_struct_members(constructor_semantic_expr.concrete_struct_id).ok()?;
let completions = struct_members
.iter()
.filter_map(|(name, data)| {
let name = name.to_string();
let visible = peek_visible_in(db, data.visibility, struct_parent_module_id, module_id);
if !visible || already_present_members.contains(&name) {
None
} else {
Some(CompletionItem {
label: name,
detail: Some(data.ty.format(db)),
kind: Some(CompletionItemKind::VALUE),
..Default::default()
})
}
})
.collect::<Vec<_>>();
if completions.is_empty() { None } else { Some(completions) }
}