From 8a253878190a4cc3112a89e338ea75ab82104d25 Mon Sep 17 00:00:00 2001 From: Tomasz Rejowski <34059895+Arcticae@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:06:30 +0200 Subject: [PATCH 1/8] Update support links (#6119) --- vscode-cairo/README.md | 3 ++- vscode-cairo/SUPPORT.md | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 vscode-cairo/SUPPORT.md diff --git a/vscode-cairo/README.md b/vscode-cairo/README.md index c621745b7c7..ac1254b10fc 100644 --- a/vscode-cairo/README.md +++ b/vscode-cairo/README.md @@ -47,7 +47,7 @@ Consult the settings UI in VS Code for more documentation. ## Support For questions or inquiries about Cairo, Cairo Language Server and this extension, reach out to us -on [Discord]. +on [Telegram] or [Discord]. ## Troubleshooting @@ -92,4 +92,5 @@ If you feel brave enough, you can try some of the more advanced debugging techni [scarb]: https://docs.swmansion.com/scarb [scarb-asdf]: https://docs.swmansion.com/scarb/download.html#install-via-asdf [scarb-dl]: https://docs.swmansion.com/scarb/download.html +[telegram]: https://t.me/cairo_ls_support [vscode-marketplace]: https://marketplace.visualstudio.com/items?itemName=starkware.cairo1 diff --git a/vscode-cairo/SUPPORT.md b/vscode-cairo/SUPPORT.md new file mode 100644 index 00000000000..0c38c212fc9 --- /dev/null +++ b/vscode-cairo/SUPPORT.md @@ -0,0 +1,16 @@ +# Support + +## How to file issues + +This project uses GitHub Issues to track bugs and feature requests. +Please search the [existing issues](https://github.com/starkware-libs/cairo/issues) before filing +new issues to avoid duplicates. +For new issues, file your bug or feature request as a new Issue. +Make sure to mention that your issue concerns language server specifically to get your request +processed quicker. + +## Contact + +If you have searched the GitHub issues for your problem, and still would like to contact the team, +we are providing support on [Telegram](https://t.me/cairo_ls_support) or +[Discord](https://discord.gg/QypNMzkHbc). From 3f2895fba2ee2a56eb9ab4262992edb926436665 Mon Sep 17 00:00:00 2001 From: Piotr Magiera <56825108+piotmag769@users.noreply.github.com> Date: Tue, 6 Aug 2024 17:04:43 +0200 Subject: [PATCH 2/8] LS: add support for struct members (#6145) --- crates/cairo-lang-doc/src/db.rs | 10 +- .../cairo-lang-doc/src/documentable_item.rs | 38 ++++++ crates/cairo-lang-doc/src/lib.rs | 1 + .../src/ide/hover/render/definition.rs | 17 ++- .../src/lang/db/syntax.rs | 15 +++ .../src/lang/inspect/defs.rs | 18 ++- crates/cairo-lang-language-server/src/lib.rs | 52 ++++++++- .../tests/e2e/goto.rs | 86 ++++++++++++++ .../tests/e2e/main.rs | 1 + .../tests/test_data/goto/struct_members.txt | 38 ++++++ .../tests/test_data/hover/basic.txt | 108 +++++++++++++++--- .../tests/test_data/hover/starknet.txt | 9 +- 12 files changed, 365 insertions(+), 28 deletions(-) create mode 100644 crates/cairo-lang-doc/src/documentable_item.rs create mode 100644 crates/cairo-lang-language-server/tests/e2e/goto.rs create mode 100644 crates/cairo-lang-language-server/tests/test_data/goto/struct_members.txt 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 8deb83b3412..5e048cce0ef 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}; @@ -878,10 +880,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. @@ -911,6 +952,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 2894758dbf5..7303b25ef18 100644 --- a/crates/cairo-lang-language-server/tests/e2e/main.rs +++ b/crates/cairo-lang-language-server/tests/e2e/main.rs @@ -1,5 +1,6 @@ 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 From dd1a4c40ab10b7c9de0cb868260ee84b8a3283d1 Mon Sep 17 00:00:00 2001 From: Piotr Figiela <77412592+Draggu@users.noreply.github.com> Date: Thu, 8 Aug 2024 11:26:28 +0200 Subject: [PATCH 3/8] LS: Fix multiline strings highlighting (#6088) --- .../src/ide/semantic_highlighting/mod.rs | 45 ++++++++++++++----- .../tests/e2e/semantic_tokens.rs | 23 ++++++++-- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/crates/cairo-lang-language-server/src/ide/semantic_highlighting/mod.rs b/crates/cairo-lang-language-server/src/ide/semantic_highlighting/mod.rs index a7e8e551531..c96ce822643 100644 --- a/crates/cairo-lang-language-server/src/ide/semantic_highlighting/mod.rs +++ b/crates/cairo-lang-language-server/src/ide/semantic_highlighting/mod.rs @@ -1,9 +1,8 @@ use cairo_lang_filesystem::span::TextOffset; use cairo_lang_parser::db::ParserGroup; use cairo_lang_syntax as syntax; -use cairo_lang_syntax::node::ast::{self}; use cairo_lang_syntax::node::kind::SyntaxKind; -use cairo_lang_syntax::node::{SyntaxNode, TypedSyntaxNode}; +use cairo_lang_syntax::node::{ast, SyntaxNode, TypedSyntaxNode}; use cairo_lang_utils::unordered_hash_map::UnorderedHashMap; use cairo_lang_utils::Upcast; use tower_lsp::lsp_types::*; @@ -69,16 +68,23 @@ impl SemanticTokensTraverser { let maybe_semantic_kind = self .offset_to_kind_lookahead .remove(&node.offset()) - .or_else(|| SemanticTokenKind::from_syntax_node(db, node)); + .or_else(|| SemanticTokenKind::from_syntax_node(db, node.clone())); + if let Some(semantic_kind) = maybe_semantic_kind { - let EncodedToken { delta_line, delta_start } = self.encoder.encode(width); - data.push(SemanticToken { - delta_line, - delta_start, - length: width, - token_type: semantic_kind.as_u32(), - token_modifiers_bitset: 0, - }); + let Some(text) = node.text(db) else { unreachable!() }; + + if text.contains('\n') { + // Split multiline token into multiple single line tokens. + for line in text.split_inclusive('\n') { + self.push_semantic_token(line.len() as u32, &semantic_kind, data); + + if line.ends_with('\n') { + self.encoder.next_line(); + } + } + } else { + self.push_semantic_token(width, &semantic_kind, data); + } } else { self.encoder.skip(width); } @@ -128,6 +134,23 @@ impl SemanticTokensTraverser { } } + fn push_semantic_token( + &mut self, + width: u32, + semantic_kind: &SemanticTokenKind, + data: &mut Vec, + ) { + let EncodedToken { delta_line, delta_start } = self.encoder.encode(width); + + data.push(SemanticToken { + delta_line, + delta_start, + length: width, + token_type: semantic_kind.as_u32(), + token_modifiers_bitset: 0, + }); + } + fn mark_future_token(&mut self, offset: TextOffset, semantic_kind: SemanticTokenKind) { self.offset_to_kind_lookahead.insert(offset, semantic_kind); } diff --git a/crates/cairo-lang-language-server/tests/e2e/semantic_tokens.rs b/crates/cairo-lang-language-server/tests/e2e/semantic_tokens.rs index 37da9964179..5fb9a04f70e 100644 --- a/crates/cairo-lang-language-server/tests/e2e/semantic_tokens.rs +++ b/crates/cairo-lang-language-server/tests/e2e/semantic_tokens.rs @@ -23,7 +23,7 @@ fn caps(base: lsp_types::ClientCapabilities) -> lsp_types::ClientCapabilities { } #[test] -fn highlights_full_file() { +fn highlights_multiline_tokens() { let mut ls = sandbox! { files { "cairo_project.toml" => r#" @@ -33,7 +33,12 @@ hello = "src" [config.global] edition = "2023_11" "#, - "src/lib.cairo" => r#"fn main() {}"#, + "src/lib.cairo" => r#" +fn main() { + let _ = " + "; +} +"#, } client_capabilities = caps; }; @@ -52,5 +57,17 @@ edition = "2023_11" let lsp_types::SemanticTokensResult::Tokens(tokens) = res else { panic!("expected full tokens") }; - assert!(tokens.data.len() > 1); + + // There is a multiline (2) string, check if 2 consecutive tokens are of type string. + assert!(tokens.data.windows(2).any(|tokens| { + let string_type = 16; // SemanticTokenKind::String.as_u32() + let first = tokens[0]; + let second = tokens[1]; + + let are_on_consecutive_lines = first.delta_line + 1 == second.delta_line; + let are_both_string = + first.token_type == second.token_type && first.token_type == string_type; + + are_both_string && are_on_consecutive_lines + })); } From 2d9b8b465797a7affcc74e00641da9d2b88be422 Mon Sep 17 00:00:00 2001 From: Piotr Magiera <56825108+piotmag769@users.noreply.github.com> Date: Thu, 8 Aug 2024 15:42:52 +0200 Subject: [PATCH 4/8] LS: fix panics in `lsp_helper` (#6168) --- .../src/ide/code_actions/add_missing_trait.rs | 2 +- .../src/ide/completion/completions.rs | 2 +- crates/cairo-lang-semantic/src/db.rs | 2 +- crates/cairo-lang-semantic/src/lsp_helpers.rs | 17 ++++++++--------- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/crates/cairo-lang-language-server/src/ide/code_actions/add_missing_trait.rs b/crates/cairo-lang-language-server/src/ide/code_actions/add_missing_trait.rs index 87924c8c010..486e058bdb2 100644 --- a/crates/cairo-lang-language-server/src/ide/code_actions/add_missing_trait.rs +++ b/crates/cairo-lang-language-server/src/ide/code_actions/add_missing_trait.rs @@ -78,7 +78,7 @@ fn missing_traits_actions( module_start_offset.position_in_file(db.upcast(), file_id).unwrap().to_lsp(); let relevant_methods = find_methods_for_type(db, resolver, ty, stable_ptr); let current_module = db.find_module_containing_node(node)?; - let module_visible_traits = db.visible_traits_from_module(current_module); + let module_visible_traits = db.visible_traits_from_module(current_module)?; let mut code_actions = vec![]; for method in relevant_methods { let method_name = method.name(db.upcast()); 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 c3db45989e1..e47c210e20d 100644 --- a/crates/cairo-lang-language-server/src/ide/completion/completions.rs +++ b/crates/cairo-lang-language-server/src/ide/completion/completions.rs @@ -274,7 +274,7 @@ pub fn completion_for_method( // If the trait is not in scope, add a use statement. if !module_has_trait(db, module_id, trait_id)? { - if let Some(trait_path) = db.visible_traits_from_module(module_id).get(&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), diff --git a/crates/cairo-lang-semantic/src/db.rs b/crates/cairo-lang-semantic/src/db.rs index cbc33f6b019..26c9f69bb36 100644 --- a/crates/cairo-lang-semantic/src/db.rs +++ b/crates/cairo-lang-semantic/src/db.rs @@ -1493,7 +1493,7 @@ pub trait SemanticGroup: fn visible_traits_from_module( &self, module_id: ModuleId, - ) -> Arc>; + ) -> Option>>; /// Returns all visible traits in a module, alongside a visible use path to the trait. /// `user_module_id` is the module from which the traits are should be visible. If /// `include_parent` is true, the parent module of `module_id` is also considered. diff --git a/crates/cairo-lang-semantic/src/lsp_helpers.rs b/crates/cairo-lang-semantic/src/lsp_helpers.rs index 518afc1a123..5bbc964530b 100644 --- a/crates/cairo-lang-semantic/src/lsp_helpers.rs +++ b/crates/cairo-lang-semantic/src/lsp_helpers.rs @@ -116,11 +116,11 @@ fn visible_traits_in_module_ex( visited_modules.insert(module_id); let mut modules_to_visit = vec![]; // Add traits and traverse modules imported into the current module. - for use_id in db.module_uses_ids(module_id).unwrap().iter().copied() { + for use_id in db.module_uses_ids(module_id).ok()?.iter().copied() { if !is_visible(use_id.name(db.upcast()))? { continue; } - let resolved_item = db.use_resolved_item(use_id).unwrap(); + let resolved_item = db.use_resolved_item(use_id).ok()?; match resolved_item { ResolvedGenericItem::Module(inner_module_id) => { modules_to_visit.push(inner_module_id); @@ -132,14 +132,14 @@ fn visible_traits_in_module_ex( } } // Traverse the submodules of the current module. - for submodule_id in db.module_submodules_ids(module_id).unwrap().iter().copied() { + for submodule_id in db.module_submodules_ids(module_id).ok()?.iter().copied() { if !is_visible(submodule_id.name(db.upcast()))? { continue; } modules_to_visit.push(ModuleId::Submodule(submodule_id)); } // Add the traits of the current module. - for trait_id in db.module_traits_ids(module_id).unwrap().iter().copied() { + for trait_id in db.module_traits_ids(module_id).ok()?.iter().copied() { if !is_visible(trait_id.name(db.upcast()))? { continue; } @@ -202,7 +202,7 @@ pub fn visible_traits_in_crate( pub fn visible_traits_from_module( db: &dyn SemanticGroup, module_id: ModuleId, -) -> Arc> { +) -> Option>> { 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()); @@ -211,11 +211,10 @@ pub fn visible_traits_from_module( ModuleId::CrateRoot(crate_id) => crate_id, ModuleId::Submodule(_) => unreachable!("current module is not a top-level module"), }; - let edition = db.crate_config(current_crate_id).unwrap().settings.edition; + let edition = db.crate_config(current_crate_id)?.settings.edition; 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).unwrap(); + let prelude_submodule = get_submodule(db, core_prelude_submodule, prelude_submodule_name)?; let mut module_visible_traits = Vec::new(); module_visible_traits.extend_from_slice( @@ -243,5 +242,5 @@ pub fn visible_traits_from_module( } } } - result.into() + Some(result.into()) } From 94518a2df7934eef3805a2b6c88c1dcfefb742b3 Mon Sep 17 00:00:00 2001 From: Piotr Magiera <56825108+piotmag769@users.noreply.github.com> Date: Thu, 8 Aug 2024 17:18:16 +0200 Subject: [PATCH 5/8] LS: fix missing module panic (#6172) --- crates/cairo-lang-language-server/src/lib.rs | 16 ++++---- .../tests/e2e/hover.rs | 1 + .../tests/test_data/hover/missing_module.txt | 38 +++++++++++++++++++ 3 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 crates/cairo-lang-language-server/tests/test_data/hover/missing_module.txt diff --git a/crates/cairo-lang-language-server/src/lib.rs b/crates/cairo-lang-language-server/src/lib.rs index 5e048cce0ef..07f438bc54e 100644 --- a/crates/cairo-lang-language-server/src/lib.rs +++ b/crates/cairo-lang-language-server/src/lib.rs @@ -948,7 +948,7 @@ fn find_definition( let item = ResolvedGenericItem::Module(ModuleId::Submodule(submodule_id)); return Some(( ResolvedItem::Generic(item.clone()), - resolved_generic_item_def(db, item), + resolved_generic_item_def(db, item)?, )); } } @@ -963,7 +963,7 @@ fn find_definition( { return Some(( ResolvedItem::Generic(item.clone()), - resolved_generic_item_def(db, item), + resolved_generic_item_def(db, item)?, )); } @@ -1005,9 +1005,9 @@ fn resolved_concrete_item_def( fn resolved_generic_item_def( db: &AnalysisDatabase, item: ResolvedGenericItem, -) -> SyntaxStablePtrId { +) -> Option { let defs_db = db.upcast(); - match item { + Some(match item { ResolvedGenericItem::GenericConstant(item) => item.untyped_stable_ptr(defs_db), ResolvedGenericItem::Module(module_id) => { // Check if the module is an inline submodule. @@ -1016,11 +1016,11 @@ fn resolved_generic_item_def( submodule_id.stable_ptr(defs_db).lookup(db.upcast()).body(db.upcast()) { // Inline module. - return submodule_id.stable_ptr().untyped(); + return Some(submodule_id.stable_ptr().untyped()); } } - let module_file = db.module_main_file(module_id).unwrap(); - let file_syntax = db.file_module_syntax(module_file).unwrap(); + let module_file = db.module_main_file(module_id).ok()?; + let file_syntax = db.file_module_syntax(module_file).ok()?; file_syntax.as_syntax_node().stable_ptr() } ResolvedGenericItem::GenericFunction(item) => { @@ -1045,7 +1045,7 @@ fn resolved_generic_item_def( trait_function.stable_ptr(defs_db).untyped() } ResolvedGenericItem::Variable(var) => var.untyped_stable_ptr(defs_db), - } + }) } fn is_cairo_file_path(file_path: &Url) -> bool { diff --git a/crates/cairo-lang-language-server/tests/e2e/hover.rs b/crates/cairo-lang-language-server/tests/e2e/hover.rs index b719e5d9c69..d758be12005 100644 --- a/crates/cairo-lang-language-server/tests/e2e/hover.rs +++ b/crates/cairo-lang-language-server/tests/e2e/hover.rs @@ -13,6 +13,7 @@ cairo_lang_test_utils::test_file_test!( "tests/test_data/hover", { basic: "basic.txt", + missing_module: "missing_module.txt", partial: "partial.txt", starknet: "starknet.txt", }, diff --git a/crates/cairo-lang-language-server/tests/test_data/hover/missing_module.txt b/crates/cairo-lang-language-server/tests/test_data/hover/missing_module.txt new file mode 100644 index 00000000000..9e113f681bd --- /dev/null +++ b/crates/cairo-lang-language-server/tests/test_data/hover/missing_module.txt @@ -0,0 +1,38 @@ +//! > Hover + +//! > test_runner_name +test_hover + +//! > cairo_project.toml +[crate_roots] +hello = "src" + +[config.global] +edition = "2023_11" + +//! > cairo_code +mod missing; + +//! > hover #0 +// = source context +mod missing; +// = highlight +No highlight information. +// = popover +No hover information. + +//! > hover #1 +// = source context +mod missing; +// = highlight +No highlight information. +// = popover +No hover information. + +//! > hover #2 +// = source context +mod missing; +// = highlight +No highlight information. +// = popover +No hover information. From f12ab96ebb6e674a6383d2239e7e993c8a9ef44c Mon Sep 17 00:00:00 2001 From: Tomasz Rejowski <34059895+Arcticae@users.noreply.github.com> Date: Thu, 8 Aug 2024 23:59:58 +0200 Subject: [PATCH 6/8] Pass plugin env variable to LS subprocess (#6176) --- vscode-cairo/src/cairols.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/vscode-cairo/src/cairols.ts b/vscode-cairo/src/cairols.ts index 15535f910e3..51f071bdaae 100644 --- a/vscode-cairo/src/cairols.ts +++ b/vscode-cairo/src/cairols.ts @@ -198,8 +198,13 @@ function setupEnv(serverExecutable: lc.Executable, ctx: Context) { const extraEnv = ctx.config.get("languageServerExtraEnv"); serverExecutable.options ??= {}; - serverExecutable.options.env ??= {}; - Object.assign(serverExecutable.options.env, logEnv, extraEnv); + serverExecutable.options.env = { + // Inherit env from parent process. + ...process.env, + ...(serverExecutable.options.env ?? {}), + ...logEnv, + ...extraEnv, + }; } function buildEnvFilter(ctx: Context): string { From cb47d699d78e33e0a800acc009cde5c8aa050153 Mon Sep 17 00:00:00 2001 From: Piotr Figiela <77412592+Draggu@users.noreply.github.com> Date: Fri, 9 Aug 2024 13:13:36 +0200 Subject: [PATCH 7/8] LS: Change Scarb and LS path settings description (#6178) --- vscode-cairo/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vscode-cairo/package.json b/vscode-cairo/package.json index 763a7513a06..e41c6b546eb 100644 --- a/vscode-cairo/package.json +++ b/vscode-cairo/package.json @@ -99,12 +99,12 @@ }, "cairo1.scarbPath": { "type": "string", - "description": "Path to the Scarb package manager binary.", + "description": "Absolute path to the Scarb package manager binary.", "scope": "window" }, "cairo1.corelibPath": { "type": "string", - "description": "Path to the Cairo core library, used in non-Scarb projects.", + "description": "Absolute path to the Cairo core library, used in non-Scarb projects.", "scope": "window" }, "cairo1.languageServerExtraEnv": { From aec15591f3a6dafc685af97296f9d78c0cdfd9bd Mon Sep 17 00:00:00 2001 From: 0xLucqs <70894690+0xLucqs@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:46:05 +0200 Subject: [PATCH 8/8] feat(span): add to_str_range for `TextSpan` (#6122) --- crates/cairo-lang-filesystem/src/span.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/cairo-lang-filesystem/src/span.rs b/crates/cairo-lang-filesystem/src/span.rs index 38244d9c456..de4d2bc42fc 100644 --- a/crates/cairo-lang-filesystem/src/span.rs +++ b/crates/cairo-lang-filesystem/src/span.rs @@ -1,5 +1,5 @@ use std::iter::Sum; -use std::ops::{Add, Sub}; +use std::ops::{Add, Range, Sub}; use crate::db::FilesGroup; use crate::ids::FileId; @@ -94,6 +94,11 @@ impl TextSpan { Self { start: self.start, end: self.start } } + /// Returns self.start..self.end as [`Range`] + pub fn to_str_range(&self) -> Range { + self.start.0.0 as usize..self.end.0.0 as usize + } + /// Convert this span to a [`TextPositionSpan`] in the file. pub fn position_in_file(self, db: &dyn FilesGroup, file: FileId) -> Option { let start = self.start.position_in_file(db, file)?;