diff --git a/crates/cairo-lang-language-server/src/ide/completion/completions.rs b/crates/cairo-lang-language-server/src/ide/completion/completions.rs index 9a860724832..287ba9c60e3 100644 --- a/crates/cairo-lang-language-server/src/ide/completion/completions.rs +++ b/crates/cairo-lang-language-server/src/ide/completion/completions.rs @@ -12,6 +12,7 @@ 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; @@ -318,3 +319,67 @@ fn module_has_trait( } Some(false) } + +/// Discovers struct members missing in the constructor call and returns completions containing +/// their names with type hints. +pub fn struct_constructor_completions( + db: &AnalysisDatabase, + lookup_items: Vec, + constructor: ast::ExprStructCtorCall, +) -> Option> { + 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)) + } + // although tail covers all remaining unspecified members, we still want to show them in + // completion. + ast::StructArg::StructArgTail(_) => None, + }) + .collect::>(); + + 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::>(); + + if completions.is_empty() { None } else { Some(completions) } +} diff --git a/crates/cairo-lang-language-server/src/ide/completion/mod.rs b/crates/cairo-lang-language-server/src/ide/completion/mod.rs index 49c2ed09ef4..f9f95d26773 100644 --- a/crates/cairo-lang-language-server/src/ide/completion/mod.rs +++ b/crates/cairo-lang-language-server/src/ide/completion/mod.rs @@ -1,3 +1,4 @@ +use cairo_lang_filesystem::ids::FileId; use cairo_lang_semantic::items::us::get_use_path_segments; use cairo_lang_semantic::resolve::AsSegments; use cairo_lang_syntax::node::ast::PathSegment; @@ -5,7 +6,8 @@ use cairo_lang_syntax::node::db::SyntaxGroup; use cairo_lang_syntax::node::kind::SyntaxKind; use cairo_lang_syntax::node::{SyntaxNode, TypedSyntaxNode, ast}; use cairo_lang_utils::Upcast; -use lsp_types::{CompletionParams, CompletionResponse, CompletionTriggerKind}; +use completions::struct_constructor_completions; +use lsp_types::{CompletionParams, CompletionResponse, CompletionTriggerKind, Position}; use tracing::debug; use self::completions::{colon_colon_completions, dot_completions, generic_completions}; @@ -36,7 +38,7 @@ pub fn complete(params: CompletionParams, db: &AnalysisDatabase) -> Option { dot_completions(db, file_id, lookup_items, expr).map(CompletionResponse::Array) } @@ -44,6 +46,10 @@ pub fn complete(params: CompletionParams, db: &AnalysisDatabase) -> Option { + struct_constructor_completions(db, lookup_items, constructor) + .map(CompletionResponse::Array) + } _ if trigger_kind == CompletionTriggerKind::INVOKED => { Some(CompletionResponse::Array(generic_completions(db, module_file_id, lookup_items))) } @@ -54,9 +60,15 @@ pub fn complete(params: CompletionParams, db: &AnalysisDatabase) -> Option), + StructConstructor(ast::ExprStructCtorCall), } -fn completion_kind(db: &AnalysisDatabase, node: SyntaxNode) -> CompletionKind { +fn completion_kind( + db: &AnalysisDatabase, + node: SyntaxNode, + position: Position, + file_id: FileId, +) -> CompletionKind { debug!("node.kind: {:#?}", node.kind(db)); match node.kind(db) { SyntaxKind::TerminalDot => { @@ -136,6 +148,49 @@ fn completion_kind(db: &AnalysisDatabase, node: SyntaxNode) -> CompletionKind { return CompletionKind::ColonColon(segments); } } + SyntaxKind::TerminalLBrace | SyntaxKind::TerminalRBrace | SyntaxKind::TerminalComma => { + if let Some(constructor_node) = + db.first_ancestor_of_kind(node, SyntaxKind::ExprStructCtorCall) + { + return CompletionKind::StructConstructor( + ast::ExprStructCtorCall::from_syntax_node(db, constructor_node), + ); + } + } + // Show completions only if struct tail is separated from the cursor by a newline. + // Exclude cases like: `..id`, `..id`, `..id` + SyntaxKind::TerminalDotDot => { + let dot_dot_node_trivia = ast::TerminalDotDot::from_syntax_node(db, node.clone()) + .leading_trivia(db) + .elements(db); + + let mut generate_completion = false; + let mut node_found_in_trivia = false; + + if let Some(node_at_position) = + db.find_syntax_node_at_position(file_id, position.to_cairo()) + { + for trivium in dot_dot_node_trivia { + if trivium.as_syntax_node() == node_at_position { + node_found_in_trivia = true; + } + + if node_found_in_trivia && matches!(trivium, ast::Trivium::Newline(_)) { + generate_completion = true; + } + } + } + + if generate_completion { + if let Some(constructor_node) = + db.first_ancestor_of_kind(node, SyntaxKind::ExprStructCtorCall) + { + return CompletionKind::StructConstructor( + ast::ExprStructCtorCall::from_syntax_node(db, constructor_node), + ); + } + } + } _ => (), } debug!("Generic"); diff --git a/crates/cairo-lang-language-server/tests/e2e/completions.rs b/crates/cairo-lang-language-server/tests/e2e/completions.rs index d36046d3258..1a0f79190ea 100644 --- a/crates/cairo-lang-language-server/tests/e2e/completions.rs +++ b/crates/cairo-lang-language-server/tests/e2e/completions.rs @@ -10,6 +10,7 @@ cairo_lang_test_utils::test_file_test!( "tests/test_data/completions", { methods_text_edits: "methods_text_edits.txt", + structs: "structs.txt", }, test_completions_text_edits diff --git a/crates/cairo-lang-language-server/tests/test_data/completions/structs.txt b/crates/cairo-lang-language-server/tests/test_data/completions/structs.txt new file mode 100644 index 00000000000..f7aeab48472 --- /dev/null +++ b/crates/cairo-lang-language-server/tests/test_data/completions/structs.txt @@ -0,0 +1,150 @@ +//! > Test completing struct members upon construction. + +//! > test_runner_name +test_completions_text_edits(detail: true) + +//! > cairo_project.toml +[crate_roots] +hello = "src" + +[config.global] +edition = "2024_07" + +//! > cairo_code +mod some_module { + pub struct Struct { + x: u32, + pub y: felt252, + pub z: i16 + } + + fn build_struct() { + let s = Struct { + x: 0x0, + y: 0x0, + z: 0x0 + }; + + let a = Struct { }; + + let b = Struct { x: 0x0, }; + + let c = Struct { + x: 0x0, + + ..s + }; + + let d = Struct { + x: 0x0, + ..s + }; + + let e = Struct { ..s }; + } +} + +mod happy_cases { + use super::some_module::Struct; + + fn foo() { + let a = Struct { }; + let b = Struct { y: 0x0, }; + let c = Struct { y: 0x0, x: 0x0, } + } +} + +mod unhappy_cases { + fn foo() { + let a = NonexsitentStruct { }; + } +} + +//! > Completions #0 + let a = Struct { }; +-------------------------- +Completion: x +Detail: core::integer::u32 +-------------------------- +Completion: y +Detail: core::felt252 +-------------------------- +Completion: z +Detail: core::integer::i16 + +//! > Completions #1 + let b = Struct { x: 0x0, }; +-------------------------- +Completion: y +Detail: core::felt252 +-------------------------- +Completion: z +Detail: core::integer::i16 + +//! > Completions #2 + +-------------------------- +Completion: y +Detail: core::felt252 +-------------------------- +Completion: z +Detail: core::integer::i16 + +//! > Completions #3 + ..s +-------------------------- +Completion: core +-------------------------- +Completion: hello +-------------------------- +Completion: Struct +-------------------------- +Completion: build_struct +-------------------------- +Completion: s +-------------------------- +Completion: a +-------------------------- +Completion: b +-------------------------- +Completion: c +-------------------------- +Completion: d +-------------------------- +Completion: e + +//! > Completions #4 + let e = Struct { ..s }; +-------------------------- +Completion: x +Detail: core::integer::u32 +-------------------------- +Completion: y +Detail: core::felt252 +-------------------------- +Completion: z +Detail: core::integer::i16 + +//! > Completions #5 + let a = Struct { }; +-------------------------- +Completion: y +Detail: core::felt252 +-------------------------- +Completion: z +Detail: core::integer::i16 + +//! > Completions #6 + let b = Struct { y: 0x0, }; +-------------------------- +Completion: z +Detail: core::integer::i16 + +//! > Completions #7 + let c = Struct { y: 0x0, x: 0x0, } +-------------------------- +Completion: z +Detail: core::integer::i16 + +//! > Completions #8 + let a = NonexsitentStruct { };