diff --git a/crates/cairo-lang-doc/src/db.rs b/crates/cairo-lang-doc/src/db.rs index b403439df2e..c729a001e48 100644 --- a/crates/cairo-lang-doc/src/db.rs +++ b/crates/cairo-lang-doc/src/db.rs @@ -1,11 +1,11 @@ use cairo_lang_defs::db::DefsGroup; -use cairo_lang_defs::ids::{LanguageElementId, LookupItemId}; use cairo_lang_parser::utils::SimpleParserDatabase; use cairo_lang_syntax::node::db::SyntaxGroup; use cairo_lang_syntax::node::kind::SyntaxKind; use cairo_lang_utils::Upcast; use itertools::Itertools; +use crate::documentable_item::DocumentableItemId; use crate::markdown::cleanup_doc_markdown; #[salsa::query_group(DocDatabase)] @@ -15,14 +15,14 @@ pub trait DocGroup: Upcast + Upcast + SyntaxGrou // be the best to convert all /// comments to #[doc] attrs before processing items by plugins, // so that plugins would get a nice and clean syntax of documentation to manipulate further. /// Gets the documentation above an item definition. - fn get_item_documentation(&self, item_id: LookupItemId) -> Option; + fn get_item_documentation(&self, item_id: DocumentableItemId) -> Option; // TODO(mkaput): Add tests. /// Gets the signature of an item (i.e., item without its body). - fn get_item_signature(&self, item_id: LookupItemId) -> String; + fn get_item_signature(&self, item_id: DocumentableItemId) -> String; } -fn get_item_documentation(db: &dyn DocGroup, item_id: LookupItemId) -> Option { +fn get_item_documentation(db: &dyn DocGroup, item_id: DocumentableItemId) -> Option { // Get the text of the item (trivia + definition) let doc = item_id.stable_location(db.upcast()).syntax_node(db.upcast()).get_text(db.upcast()); @@ -57,7 +57,7 @@ fn get_item_documentation(db: &dyn DocGroup, item_id: LookupItemId) -> Option String { +fn get_item_signature(db: &dyn DocGroup, item_id: DocumentableItemId) -> String { let syntax_node = item_id.stable_location(db.upcast()).syntax_node(db.upcast()); let definition = match syntax_node.green_node(db.upcast()).kind { SyntaxKind::ItemConstant diff --git a/crates/cairo-lang-doc/src/documentable_item.rs b/crates/cairo-lang-doc/src/documentable_item.rs new file mode 100644 index 00000000000..00ed89a41dd --- /dev/null +++ b/crates/cairo-lang-doc/src/documentable_item.rs @@ -0,0 +1,38 @@ +use cairo_lang_defs::db::DefsGroup; +use cairo_lang_defs::diagnostic_utils::StableLocation; +use cairo_lang_defs::ids::{LanguageElementId, LookupItemId, MemberId, VariantId}; + +/// Item which documentation can be fetched from source code. +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub enum DocumentableItemId { + LookupItem(LookupItemId), + Member(MemberId), + Variant(VariantId), +} + +impl DocumentableItemId { + pub fn stable_location(&self, db: &dyn DefsGroup) -> StableLocation { + match self { + DocumentableItemId::LookupItem(lookup_item_id) => lookup_item_id.stable_location(db), + DocumentableItemId::Member(member_id) => member_id.stable_location(db), + DocumentableItemId::Variant(variant_id) => variant_id.stable_location(db), + } + } +} + +impl From for DocumentableItemId { + fn from(value: LookupItemId) -> Self { + DocumentableItemId::LookupItem(value) + } +} + +impl From for DocumentableItemId { + fn from(value: MemberId) -> Self { + DocumentableItemId::Member(value) + } +} +impl From for DocumentableItemId { + fn from(value: VariantId) -> Self { + DocumentableItemId::Variant(value) + } +} diff --git a/crates/cairo-lang-doc/src/lib.rs b/crates/cairo-lang-doc/src/lib.rs index 1c5aa321b19..979e6dad9fe 100644 --- a/crates/cairo-lang-doc/src/lib.rs +++ b/crates/cairo-lang-doc/src/lib.rs @@ -1,2 +1,3 @@ pub mod db; +pub mod documentable_item; mod markdown; diff --git a/crates/cairo-lang-language-server/src/ide/hover/render/definition.rs b/crates/cairo-lang-language-server/src/ide/hover/render/definition.rs index 56841f21ff2..6e7475b3f3b 100644 --- a/crates/cairo-lang-language-server/src/ide/hover/render/definition.rs +++ b/crates/cairo-lang-language-server/src/ide/hover/render/definition.rs @@ -1,4 +1,5 @@ use cairo_lang_defs::db::DefsGroup; +use cairo_lang_doc::db::DocGroup; use cairo_lang_filesystem::ids::FileId; use cairo_lang_syntax::node::ast::TerminalIdentifier; use cairo_lang_syntax::node::TypedSyntaxNode; @@ -7,7 +8,7 @@ use tower_lsp::lsp_types::Hover; use crate::ide::hover::markdown_contents; use crate::lang::db::AnalysisDatabase; -use crate::lang::inspect::defs::SymbolDef; +use crate::lang::inspect::defs::{MemberDef, SymbolDef}; use crate::lang::lsp::ToLsp; use crate::markdown::{fenced_code_block, RULE}; @@ -41,6 +42,20 @@ pub fn definition( } md } + SymbolDef::Member(MemberDef { member, structure }) => { + let mut md = String::new(); + + // Signature is the signature of the struct, so it makes sense that the definition + // path is too. + md += &fenced_code_block(&structure.definition_path(db)); + md += &fenced_code_block(&structure.signature(db)); + + if let Some(doc) = db.get_item_documentation((*member).into()) { + md += RULE; + md += &doc; + } + md + } }; Some(Hover { diff --git a/crates/cairo-lang-language-server/src/lang/db/syntax.rs b/crates/cairo-lang-language-server/src/lang/db/syntax.rs index 4461bfc86d9..7d67c54f7ff 100644 --- a/crates/cairo-lang-language-server/src/lang/db/syntax.rs +++ b/crates/cairo-lang-language-server/src/lang/db/syntax.rs @@ -51,6 +51,21 @@ pub trait LsSyntaxGroup: Upcast { find(TextPosition { col, ..position }) }) } + + /// Finds first ancestor of a given kind. + fn first_ancestor_of_kind(&self, mut node: SyntaxNode, kind: SyntaxKind) -> Option { + let db = self.upcast(); + let syntax_db = db.upcast(); + + while let Some(parent) = node.parent() { + if parent.kind(syntax_db) == kind { + return Some(parent); + } else { + node = parent; + } + } + None + } } impl LsSyntaxGroup for T where T: Upcast + ?Sized {} diff --git a/crates/cairo-lang-language-server/src/lang/inspect/defs.rs b/crates/cairo-lang-language-server/src/lang/inspect/defs.rs index a8266f58e1c..c94752bb1f9 100644 --- a/crates/cairo-lang-language-server/src/lang/inspect/defs.rs +++ b/crates/cairo-lang-language-server/src/lang/inspect/defs.rs @@ -1,7 +1,7 @@ use std::iter; use cairo_lang_defs::ids::{ - LanguageElementId, LookupItemId, ModuleItemId, TopLevelLanguageElementId, TraitItemId, + LanguageElementId, LookupItemId, MemberId, ModuleItemId, TopLevelLanguageElementId, TraitItemId, }; use cairo_lang_doc::db::DocGroup; use cairo_lang_semantic::db::SemanticGroup; @@ -20,6 +20,7 @@ use smol_str::SmolStr; use tracing::error; use crate::lang::db::{AnalysisDatabase, LsSemanticGroup}; +use crate::lang::inspect::defs::SymbolDef::Member; use crate::{find_definition, ResolvedItem}; /// Keeps information about the symbol that is being searched for/inspected. @@ -30,6 +31,13 @@ pub enum SymbolDef { Item(ItemDef), Variable(VariableDef), ExprInlineMacro(String), + Member(MemberDef), +} + +/// Information about a struct member. +pub struct MemberDef { + pub member: MemberId, + pub structure: ItemDef, } impl SymbolDef { @@ -81,6 +89,10 @@ impl SymbolDef { ResolvedItem::Generic(ResolvedGenericItem::Variable(_)) => { VariableDef::new(db, definition_node).map(Self::Variable) } + ResolvedItem::Member(member_id) => Some(Member(MemberDef { + member: member_id, + structure: ItemDef::new(db, &definition_node)?, + })), } } } @@ -129,12 +141,12 @@ impl ItemDef { pub fn signature(&self, db: &AnalysisDatabase) -> String { let contexts = self.context_items.iter().copied().rev(); let this = iter::once(self.lookup_item_id); - contexts.chain(this).map(|item| db.get_item_signature(item)).join("\n") + contexts.chain(this).map(|item| db.get_item_signature(item.into())).join("\n") } /// Gets item documentation in a final form usable for display. pub fn documentation(&self, db: &AnalysisDatabase) -> Option { - db.get_item_documentation(self.lookup_item_id) + db.get_item_documentation(self.lookup_item_id.into()) } /// Gets the full path (including crate name and defining trait/impl if applicable) diff --git a/crates/cairo-lang-language-server/src/lib.rs b/crates/cairo-lang-language-server/src/lib.rs index bf57d28d0e3..4949bb3b7fd 100644 --- a/crates/cairo-lang-language-server/src/lib.rs +++ b/crates/cairo-lang-language-server/src/lib.rs @@ -49,7 +49,7 @@ use anyhow::{bail, Context}; use cairo_lang_compiler::project::{setup_project, update_crate_roots_from_project_config}; use cairo_lang_defs::db::DefsGroup; use cairo_lang_defs::ids::{ - FunctionTitleId, LanguageElementId, LookupItemId, ModuleId, SubmoduleLongId, + FunctionTitleId, LanguageElementId, LookupItemId, MemberId, ModuleId, SubmoduleLongId, }; use cairo_lang_diagnostics::Diagnostics; use cairo_lang_filesystem::db::{ @@ -63,11 +63,13 @@ use cairo_lang_parser::db::ParserGroup; use cairo_lang_parser::ParserDiagnostic; use cairo_lang_project::ProjectConfig; use cairo_lang_semantic::db::SemanticGroup; +use cairo_lang_semantic::items::function_with_body::SemanticExprLookup; use cairo_lang_semantic::items::functions::GenericFunctionId; use cairo_lang_semantic::items::imp::ImplLongId; +use cairo_lang_semantic::lookup_item::LookupItemEx; use cairo_lang_semantic::plugin::PluginSuite; use cairo_lang_semantic::resolve::{ResolvedConcreteItem, ResolvedGenericItem}; -use cairo_lang_semantic::{SemanticDiagnostic, TypeLongId}; +use cairo_lang_semantic::{Expr, SemanticDiagnostic, TypeLongId}; use cairo_lang_syntax::node::ids::SyntaxStablePtrId; use cairo_lang_syntax::node::kind::SyntaxKind; use cairo_lang_syntax::node::{ast, TypedStablePtr, TypedSyntaxNode}; @@ -868,10 +870,49 @@ impl LanguageServer for Backend { } } -/// Either [`ResolvedGenericItem`] or [`ResolvedConcreteItem`]. +/// Extracts [`MemberId`] if the [`ast::TerminalIdentifier`] points to +/// right-hand side of access member expression e.g., to `xyz` in `self.xyz`. +fn try_extract_member( + db: &AnalysisDatabase, + identifier: &ast::TerminalIdentifier, + lookup_items: &[LookupItemId], +) -> Option { + let syntax_node = identifier.as_syntax_node(); + let binary_expr_syntax_node = + db.first_ancestor_of_kind(syntax_node.clone(), SyntaxKind::ExprBinary)?; + let binary_expr = ast::ExprBinary::from_syntax_node(db, binary_expr_syntax_node); + + let function_with_body = lookup_items.first()?.function_with_body()?; + + let expr_id = + db.lookup_expr_by_ptr(function_with_body, binary_expr.stable_ptr().into()).ok()?; + let semantic_expr = db.expr_semantic(function_with_body, expr_id); + + if let Expr::MemberAccess(expr_member_access) = semantic_expr { + let pointer_to_rhs = binary_expr.rhs(db).stable_ptr().untyped(); + + let mut current_node = syntax_node; + // Check if the terminal identifier points to a member, not a struct variable. + while pointer_to_rhs != current_node.stable_ptr() { + // If we found the node with the binary expression, then we are sure we won't find the + // node with the member. + if current_node.stable_ptr() == binary_expr.stable_ptr().untyped() { + return None; + } + current_node = current_node.parent().unwrap(); + } + + Some(expr_member_access.member) + } else { + None + } +} + +/// Either [`ResolvedGenericItem`], [`ResolvedConcreteItem`] or [`MemberId`]. enum ResolvedItem { Generic(ResolvedGenericItem), Concrete(ResolvedConcreteItem), + Member(MemberId), } // TODO(mkaput): Move this to crate::lang::inspect::defs and make private. @@ -901,6 +942,11 @@ fn find_definition( )); } } + + if let Some(member_id) = try_extract_member(db, identifier, lookup_items) { + return Some((ResolvedItem::Member(member_id), member_id.untyped_stable_ptr(db))); + } + for lookup_item_id in lookup_items.iter().copied() { if let Some(item) = db.lookup_resolved_generic_item_by_ptr(lookup_item_id, identifier.stable_ptr()) diff --git a/crates/cairo-lang-language-server/tests/e2e/goto.rs b/crates/cairo-lang-language-server/tests/e2e/goto.rs new file mode 100644 index 00000000000..b750f3fb32a --- /dev/null +++ b/crates/cairo-lang-language-server/tests/e2e/goto.rs @@ -0,0 +1,86 @@ +use cairo_lang_test_utils::parse_test_file::TestRunnerResult; +use cairo_lang_utils::ordered_hash_map::OrderedHashMap; +use tower_lsp::lsp_types::{ + lsp_request, ClientCapabilities, GotoCapability, GotoDefinitionParams, GotoDefinitionResponse, + TextDocumentClientCapabilities, TextDocumentIdentifier, TextDocumentPositionParams, +}; + +use crate::support::cursor::{peek_caret, peek_selection}; +use crate::support::{cursors, sandbox}; + +cairo_lang_test_utils::test_file_test!( + goto, + "tests/test_data/goto", + { + struct_members: "struct_members.txt", + }, + test_goto_members +); + +fn caps(base: ClientCapabilities) -> ClientCapabilities { + ClientCapabilities { + text_document: base.text_document.or_else(Default::default).map(|it| { + TextDocumentClientCapabilities { + definition: Some(GotoCapability { + dynamic_registration: Some(false), + link_support: None, + }), + ..it + } + }), + ..base + } +} + +/// Perform hover test. +/// +/// 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 goto definition information at each caret position and compares +/// the result with the expected hover information from the snapshot file. +fn test_goto_members( + 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(), + } + client_capabilities = caps; + }; + ls.open_and_wait_for_diagnostics("src/lib.cairo"); + + let mut goto_definitions = OrderedHashMap::default(); + + for (n, position) in cursors.carets().into_iter().enumerate() { + let mut report = String::new(); + + report.push_str(&peek_caret(&cairo, position)); + let code_action_params = GotoDefinitionParams { + text_document_position_params: TextDocumentPositionParams { + text_document: TextDocumentIdentifier { uri: ls.doc_id("src/lib.cairo").uri }, + position, + }, + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + }; + let goto_definition_response = + ls.send_request::(code_action_params); + + if let Some(goto_definition_response) = goto_definition_response { + if let GotoDefinitionResponse::Scalar(location) = goto_definition_response { + report.push_str(&peek_selection(&cairo, &location.range)); + } else { + panic!("Unexpected GotoDefinitionResponse variant.") + } + } else { + panic!("Goto definition request failed."); + } + goto_definitions.insert(format!("Goto definition #{}", n), report); + } + + TestRunnerResult::success(goto_definitions) +} diff --git a/crates/cairo-lang-language-server/tests/e2e/main.rs b/crates/cairo-lang-language-server/tests/e2e/main.rs index 311b398d80d..4d57496aef8 100644 --- a/crates/cairo-lang-language-server/tests/e2e/main.rs +++ b/crates/cairo-lang-language-server/tests/e2e/main.rs @@ -1,6 +1,7 @@ mod analysis; mod code_actions; mod completions; +mod goto; mod hover; mod semantic_tokens; mod support; diff --git a/crates/cairo-lang-language-server/tests/test_data/goto/struct_members.txt b/crates/cairo-lang-language-server/tests/test_data/goto/struct_members.txt new file mode 100644 index 00000000000..0197c11861a --- /dev/null +++ b/crates/cairo-lang-language-server/tests/test_data/goto/struct_members.txt @@ -0,0 +1,38 @@ +//! > Test simple goto definition on struct members. + +//! > test_runner_name +test_goto_members + +//! > cairo_project.toml +[crate_roots] +hello = "src" + +[config.global] +edition = "2024_07" + +//! > cairo_code +#[derive(Drop)] +struct Rectangle { + width: u64, + height: u64, +} + +fn calculate_area(rectangle: Rectangle) -> u64 { + rectangle.width * rectangle.height +} + +//! > Goto definition #0 + rectangle.width * rectangle.height +fn calculate_area(rectangle: Rectangle) -> u64 { + +//! > Goto definition #1 + rectangle.width * rectangle.height + width: u64, + +//! > Goto definition #2 + rectangle.width * rectangle.height +fn calculate_area(rectangle: Rectangle) -> u64 { + +//! > Goto definition #3 + rectangle.width * rectangle.height + height: u64, diff --git a/crates/cairo-lang-language-server/tests/test_data/hover/basic.txt b/crates/cairo-lang-language-server/tests/test_data/hover/basic.txt index e01076b7ccd..420fca4957f 100644 --- a/crates/cairo-lang-language-server/tests/test_data/hover/basic.txt +++ b/crates/cairo-lang-language-server/tests/test_data/hover/basic.txt @@ -43,7 +43,7 @@ trait RectangleTrait { /// Implementing the `RectangleTrait` for the `Rectangle` struct. impl RectangleImpl of RectangleTrait { fn area(self: @Rectangle) -> u64 { - (*self.width) * (*self.height) + (*self.width) * (*self.height) } } @@ -323,15 +323,85 @@ Rectangle struct. //! > hover #19 // = source context - (*self.width) * (*self.height) + (*self.width) * (*self.height) // = highlight -No highlight information. + (*self.width) * (*self.height) // = popover ```cairo -@core::integer::u64 +self: @hello::Rectangle ``` //! > hover #20 +// = source context + (*self.width) * (*self.height) +// = highlight + (*self.width) * (*self.height) +// = popover +```cairo +hello +``` +```cairo +struct Rectangle { + /// Width of the rectangle. + width: u64, + /// Height of the rectangle. + height: u64, +} +``` +--- +Width of the rectangle. + +//! > hover #21 +// = source context + (*self.width) * (*self.height) +// = highlight + (*self.width) * (*self.height) +// = popover +```cairo +hello +``` +```cairo +struct Rectangle { + /// Width of the rectangle. + width: u64, + /// Height of the rectangle. + height: u64, +} +``` +--- +Width of the rectangle. + +//! > hover #22 +// = source context + (*self.width) * (*self.height) +// = highlight + (*self.width) * (*self.height) +// = popover +```cairo +self: @hello::Rectangle +``` + +//! > hover #23 +// = source context + (*self.width) * (*self.height) +// = highlight + (*self.width) * (*self.height) +// = popover +```cairo +hello +``` +```cairo +struct Rectangle { + /// Width of the rectangle. + width: u64, + /// Height of the rectangle. + height: u64, +} +``` +--- +Height of the rectangle. + +//! > hover #24 // = source context fn area2(self: @Rectangle) -> u64 { // = highlight @@ -351,17 +421,27 @@ struct Rectangle { --- Rectangle struct. -//! > hover #21 +//! > hover #25 // = source context (*self.width) * (*self.height) // = highlight -No highlight information. + (*self.width) * (*self.height) // = popover ```cairo -@core::integer::u64 +hello ``` +```cairo +struct Rectangle { + /// Width of the rectangle. + width: u64, + /// Height of the rectangle. + height: u64, +} +``` +--- +Width of the rectangle. -//! > hover #22 +//! > hover #26 // = source context enum Coin { // = highlight @@ -369,7 +449,7 @@ No highlight information. // = popover No hover information. -//! > hover #23 +//! > hover #27 // = source context Penny, // = highlight @@ -377,21 +457,21 @@ No highlight information. // = popover No hover information. -//! > hover #24 +//! > hover #28 // = source context fn value_in_cents(coin: Coin) -> felt252 { // = highlight No highlight information. // = popover -//! > hover #25 +//! > hover #29 // = source context fn value_in_cents(coin: Coin) -> felt252 { // = highlight No highlight information. // = popover -//! > hover #26 +//! > hover #30 // = source context fn value_in_cents(coin: Coin) -> felt252 { // = highlight @@ -406,7 +486,7 @@ enum Coin { } ``` -//! > hover #27 +//! > hover #31 // = source context match coin { // = highlight @@ -416,7 +496,7 @@ enum Coin { coin: hello::Coin ``` -//! > hover #28 +//! > hover #32 // = source context Coin::Penny => 1, // = highlight diff --git a/crates/cairo-lang-language-server/tests/test_data/hover/starknet.txt b/crates/cairo-lang-language-server/tests/test_data/hover/starknet.txt index 11bb2b935a2..06d9b2cd2b9 100644 --- a/crates/cairo-lang-language-server/tests/test_data/hover/starknet.txt +++ b/crates/cairo-lang-language-server/tests/test_data/hover/starknet.txt @@ -86,10 +86,15 @@ pub struct ContractState {} // = source context self.value.write(value_); // = highlight -No highlight information. + self.value.write(value_); // = popover ```cairo -core::starknet::storage::storage_base::StorageBase::> +hello +``` +```cairo +pub struct StorageStorageBaseMut { + pub value: starknet::storage::StorageBase>, +} ``` //! > hover #4