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 3ef72e2e074..c3db45989e1 100644 --- a/crates/cairo-lang-language-server/src/ide/completion/completions.rs +++ b/crates/cairo-lang-language-server/src/ide/completion/completions.rs @@ -6,11 +6,13 @@ use cairo_lang_defs::ids::{ 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::structure::SemanticStructEx; +use cairo_lang_semantic::items::us::SemanticUseEx; use cairo_lang_semantic::lookup_item::{HasResolverData, LookupItemEx}; use cairo_lang_semantic::resolve::{ResolvedConcreteItem, ResolvedGenericItem, Resolver}; use cairo_lang_semantic::types::peel_snapshots; @@ -271,11 +273,13 @@ pub fn completion_for_method( let mut additional_text_edits = vec![]; // If the trait is not in scope, add a use statement. - if let Some(trait_path) = db.visible_traits_from_module(module_id).get(&trait_id) { - additional_text_edits.push(TextEdit { - range: Range::new(position, position), - new_text: format!("use {};\n", trait_path), - }); + if !module_has_trait(db, module_id, trait_id)? { + if let Some(trait_path) = db.visible_traits_from_module(module_id).get(&trait_id) { + additional_text_edits.push(TextEdit { + range: Range::new(position, position), + new_text: format!("use {};\n", trait_path), + }); + } } let completion = CompletionItem { @@ -288,3 +292,36 @@ pub fn completion_for_method( }; Some(completion) } + +/// Checks if a module has a trait in scope. +#[tracing::instrument(level = "trace", skip_all)] +fn module_has_trait( + db: &AnalysisDatabase, + module_id: ModuleId, + trait_id: cairo_lang_defs::ids::TraitId, +) -> Option { + 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) +} diff --git a/crates/cairo-lang-language-server/tests/e2e/completions.rs b/crates/cairo-lang-language-server/tests/e2e/completions.rs new file mode 100644 index 00000000000..840e94eac45 --- /dev/null +++ b/crates/cairo-lang-language-server/tests/e2e/completions.rs @@ -0,0 +1,76 @@ +use cairo_lang_test_utils::parse_test_file::TestRunnerResult; +use cairo_lang_utils::ordered_hash_map::OrderedHashMap; +use tower_lsp::lsp_types::{lsp_request, CompletionParams, TextDocumentPositionParams}; + +use crate::support::cursor::peek_caret; +use crate::support::{cursors, sandbox}; + +cairo_lang_test_utils::test_file_test!( + completions, + "tests/test_data/completions", + { + methods_text_edits: "methods_text_edits.txt", + }, + test_completions_text_edits + +); + +/// Perform completions text edits test. Notice that the test shows many possible completions, +/// however in practice only those who have the same prefix as the existing code are shown. +/// +/// This function spawns a sandbox language server with the given code in the `src/lib.cairo` file. +/// The Cairo source code is expected to contain caret markers. +/// The function then requests quick fixes at each caret position and compares the result with the +/// expected quick fixes from the snapshot file. +fn test_completions_text_edits( + inputs: &OrderedHashMap, + _args: &OrderedHashMap, +) -> TestRunnerResult { + let (cairo, cursors) = cursors(&inputs["cairo_code"]); + + let mut ls = sandbox! { + files { + "cairo_project.toml" => inputs["cairo_project.toml"].clone(), + "src/lib.cairo" => cairo.clone(), + } + }; + + ls.open("src/lib.cairo"); + + let mut completions = OrderedHashMap::default(); + + for (n, position) in cursors.carets().into_iter().enumerate() { + let mut report = String::new(); + + report.push_str(&peek_caret(&cairo, position)); + let completion_params = CompletionParams { + text_document_position: TextDocumentPositionParams { + text_document: ls.doc_id("src/lib.cairo"), + position, + }, + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + context: None, + }; + let caret_completions = + ls.send_request::(completion_params); + if let Some(completions) = caret_completions { + let completion_items = match completions { + tower_lsp::lsp_types::CompletionResponse::Array(items) => items, + tower_lsp::lsp_types::CompletionResponse::List(list) => list.items, + }; + for completion in completion_items { + if let Some(text_edit) = completion.additional_text_edits { + report.push_str("--------------------------\n"); + report.push_str(format!("Completion: {}\n", completion.label).as_str()); + for edit in text_edit { + report.push_str(format!("Text edit: {}", edit.new_text).as_str()); + } + } + } + } + completions.insert(format!("Completions #{}", n), report); + } + + TestRunnerResult::success(completions) +} diff --git a/crates/cairo-lang-language-server/tests/e2e/main.rs b/crates/cairo-lang-language-server/tests/e2e/main.rs index 0c096fa30ca..2894758dbf5 100644 --- a/crates/cairo-lang-language-server/tests/e2e/main.rs +++ b/crates/cairo-lang-language-server/tests/e2e/main.rs @@ -1,4 +1,5 @@ mod code_actions; +mod completions; mod hover; mod semantic_tokens; mod support; diff --git a/crates/cairo-lang-language-server/tests/test_data/completions/methods_text_edits.txt b/crates/cairo-lang-language-server/tests/test_data/completions/methods_text_edits.txt new file mode 100644 index 00000000000..7a1e34878ba --- /dev/null +++ b/crates/cairo-lang-language-server/tests/test_data/completions/methods_text_edits.txt @@ -0,0 +1,206 @@ +//! > Test adding simple trait. + +//! > test_runner_name +test_completions_text_edits + +//! > cairo_project.toml +[crate_roots] +hello = "src" + +[config.global] +edition = "2024_07" + +//! > cairo_code +mod hidden_trait { + pub trait ATrait1 { + fn some_method(self: @T); + } + impl Felt252ATraitImpl of ATrait1 { + fn some_method(self: @felt252) {} + } +} + +use hidden_trait::ATrait1; + +mod inner_mod { + fn main() { + let x = 5_felt252; + x.some_me + } +} + +//! > Completions #0 + x.some_me +-------------------------- +Completion: add_eq() +Text edit: use core::traits::AddEq; +-------------------------- +Completion: sub_eq() +Text edit: use core::traits::SubEq; +-------------------------- +Completion: mul_eq() +Text edit: use core::traits::MulEq; +-------------------------- +Completion: into() +-------------------------- +Completion: try_into() +-------------------------- +Completion: destruct() +-------------------------- +Completion: panic_destruct() +-------------------------- +Completion: new_inputs() +Text edit: use core::circuit::CircuitInputs; +-------------------------- +Completion: get_descriptor() +-------------------------- +Completion: clone() +-------------------------- +Completion: is_zero() +Text edit: use core::num::traits::Zero; +-------------------------- +Completion: is_non_zero() +Text edit: use core::num::traits::Zero; +-------------------------- +Completion: is_one() +Text edit: use core::num::traits::One; +-------------------------- +Completion: is_non_one() +Text edit: use core::num::traits::One; +-------------------------- +Completion: add_assign() +Text edit: use core::ops::AddAssign; +-------------------------- +Completion: sub_assign() +Text edit: use core::ops::SubAssign; +-------------------------- +Completion: mul_assign() +Text edit: use core::ops::MulAssign; +-------------------------- +Completion: serialize() +-------------------------- +Completion: print() +-------------------------- +Completion: fmt() +Text edit: use core::fmt::Display; +-------------------------- +Completion: fmt() +Text edit: use core::fmt::Debug; +-------------------------- +Completion: is_zero() +-------------------------- +Completion: is_non_zero() +-------------------------- +Completion: append_formatted_to_byte_array() +Text edit: use core::to_byte_array::AppendFormattedToByteArray; +-------------------------- +Completion: format_as_byte_array() +Text edit: use core::to_byte_array::FormatAsByteArray; +-------------------------- +Completion: some_method() +Text edit: use super::ATrait1; + +//! > ========================================================================== + +//! > Test adding non directly visible traits. + +//! > test_runner_name +test_completions_text_edits + +//! > cairo_project.toml +[crate_roots] +hello = "src" + +[config.global] +edition = "2024_07" + +//! > cairo_code +mod hidden_trait { + + pub trait ATrait1 { + fn some_method(self: @T); + } + impl Felt252ATraitImpl of ATrait1 { + fn some_method(self: @felt252) {} + } +} + +use hidden_trait::ATrait1; + +mod inner_mod { + fn main() { + let x = 5_felt252; + x.some_me + } +} + +//! > Completions #0 + x.some_me +-------------------------- +Completion: add_eq() +Text edit: use core::traits::AddEq; +-------------------------- +Completion: sub_eq() +Text edit: use core::traits::SubEq; +-------------------------- +Completion: mul_eq() +Text edit: use core::traits::MulEq; +-------------------------- +Completion: into() +-------------------------- +Completion: try_into() +-------------------------- +Completion: destruct() +-------------------------- +Completion: panic_destruct() +-------------------------- +Completion: new_inputs() +Text edit: use core::circuit::CircuitInputs; +-------------------------- +Completion: get_descriptor() +-------------------------- +Completion: clone() +-------------------------- +Completion: is_zero() +Text edit: use core::num::traits::Zero; +-------------------------- +Completion: is_non_zero() +Text edit: use core::num::traits::Zero; +-------------------------- +Completion: is_one() +Text edit: use core::num::traits::One; +-------------------------- +Completion: is_non_one() +Text edit: use core::num::traits::One; +-------------------------- +Completion: add_assign() +Text edit: use core::ops::AddAssign; +-------------------------- +Completion: sub_assign() +Text edit: use core::ops::SubAssign; +-------------------------- +Completion: mul_assign() +Text edit: use core::ops::MulAssign; +-------------------------- +Completion: serialize() +-------------------------- +Completion: print() +-------------------------- +Completion: fmt() +Text edit: use core::fmt::Display; +-------------------------- +Completion: fmt() +Text edit: use core::fmt::Debug; +-------------------------- +Completion: is_zero() +-------------------------- +Completion: is_non_zero() +-------------------------- +Completion: append_formatted_to_byte_array() +Text edit: use core::to_byte_array::AppendFormattedToByteArray; +-------------------------- +Completion: format_as_byte_array() +Text edit: use core::to_byte_array::FormatAsByteArray; +-------------------------- +Completion: some_method() +Text edit: use super::ATrait1;