Skip to content

Commit

Permalink
LS: Add completions of struct members
Browse files Browse the repository at this point in the history
commit-id:9fb1df2c
  • Loading branch information
integraledelebesgue committed Nov 14, 2024
1 parent bf9abe3 commit e0584da
Show file tree
Hide file tree
Showing 4 changed files with 274 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<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))
}
// although tail covers all remaining unspecified members, we still want to show them in
// completion.
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) }
}
61 changes: 58 additions & 3 deletions crates/cairo-lang-language-server/src/ide/completion/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
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;
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};
Expand Down Expand Up @@ -36,14 +38,18 @@ pub fn complete(params: CompletionParams, db: &AnalysisDatabase) -> Option<Compl
let trigger_kind =
params.context.map(|it| it.trigger_kind).unwrap_or(CompletionTriggerKind::INVOKED);

match completion_kind(db, node) {
match completion_kind(db, node, position, file_id) {
CompletionKind::Dot(expr) => {
dot_completions(db, file_id, lookup_items, expr).map(CompletionResponse::Array)
}
CompletionKind::ColonColon(segments) if !segments.is_empty() => {
colon_colon_completions(db, module_file_id, lookup_items, segments)
.map(CompletionResponse::Array)
}
CompletionKind::StructConstructor(constructor) => {
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)))
}
Expand All @@ -54,9 +60,15 @@ pub fn complete(params: CompletionParams, db: &AnalysisDatabase) -> Option<Compl
enum CompletionKind {
Dot(ast::ExprBinary),
ColonColon(Vec<PathSegment>),
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 => {
Expand Down Expand Up @@ -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: `<cursor>..id`, `.<cursor>.id`, `..<cursor>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");
Expand Down
1 change: 1 addition & 0 deletions crates/cairo-lang-language-server/tests/e2e/completions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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 { <caret> };

let b = Struct { x: 0x0, <caret> };

let c = Struct {
x: 0x0,
<caret>
..s
};

let d = Struct {
x: 0x0,
<caret>..s
};

let e = Struct { <caret>..s };
}
}

mod happy_cases {
use super::some_module::Struct;

fn foo() {
let a = Struct { <caret> };
let b = Struct { y: 0x0, <caret> };
let c = Struct { y: 0x0, x: 0x0, <caret> }
}
}

mod unhappy_cases {
fn foo() {
let a = NonexsitentStruct { <caret> };
}
}

//! > Completions #0
let a = Struct { <caret> };
--------------------------
Completion: x
Detail: core::integer::u32
--------------------------
Completion: y
Detail: core::felt252
--------------------------
Completion: z
Detail: core::integer::i16

//! > Completions #1
let b = Struct { x: 0x0, <caret> };
--------------------------
Completion: y
Detail: core::felt252
--------------------------
Completion: z
Detail: core::integer::i16

//! > Completions #2
<caret>
--------------------------
Completion: y
Detail: core::felt252
--------------------------
Completion: z
Detail: core::integer::i16

//! > Completions #3
<caret>..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 { <caret>..s };
--------------------------
Completion: x
Detail: core::integer::u32
--------------------------
Completion: y
Detail: core::felt252
--------------------------
Completion: z
Detail: core::integer::i16

//! > Completions #5
let a = Struct { <caret> };
--------------------------
Completion: y
Detail: core::felt252
--------------------------
Completion: z
Detail: core::integer::i16

//! > Completions #6
let b = Struct { y: 0x0, <caret> };
--------------------------
Completion: z
Detail: core::integer::i16

//! > Completions #7
let c = Struct { y: 0x0, x: 0x0, <caret> }
--------------------------
Completion: z
Detail: core::integer::i16

//! > Completions #8
let a = NonexsitentStruct { <caret> };

0 comments on commit e0584da

Please sign in to comment.