Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LS: add support for struct members #6145

Merged
merged 4 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions crates/cairo-lang-doc/src/db.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -15,14 +15,14 @@ pub trait DocGroup: Upcast<dyn DefsGroup> + Upcast<dyn SyntaxGroup> + 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<String>;
fn get_item_documentation(&self, item_id: DocumentableItemId) -> Option<String>;

// 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<String> {
fn get_item_documentation(db: &dyn DocGroup, item_id: DocumentableItemId) -> Option<String> {
// Get the text of the item (trivia + definition)
let doc = item_id.stable_location(db.upcast()).syntax_node(db.upcast()).get_text(db.upcast());

Expand Down Expand Up @@ -57,7 +57,7 @@ fn get_item_documentation(db: &dyn DocGroup, item_id: LookupItemId) -> Option<St
(!doc.trim().is_empty()).then_some(doc)
}

fn get_item_signature(db: &dyn DocGroup, item_id: LookupItemId) -> 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
Expand Down
38 changes: 38 additions & 0 deletions crates/cairo-lang-doc/src/documentable_item.rs
Original file line number Diff line number Diff line change
@@ -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<LookupItemId> for DocumentableItemId {
fn from(value: LookupItemId) -> Self {
DocumentableItemId::LookupItem(value)
}
}

impl From<MemberId> for DocumentableItemId {
fn from(value: MemberId) -> Self {
DocumentableItemId::Member(value)
}
}
impl From<VariantId> for DocumentableItemId {
fn from(value: VariantId) -> Self {
DocumentableItemId::Variant(value)
}
}
1 change: 1 addition & 0 deletions crates/cairo-lang-doc/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod db;
pub mod documentable_item;
mod markdown;
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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};

Expand Down Expand Up @@ -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 {
Expand Down
15 changes: 15 additions & 0 deletions crates/cairo-lang-language-server/src/lang/db/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ pub trait LsSyntaxGroup: Upcast<dyn ParserGroup> {
find(TextPosition { col, ..position })
})
}

/// Finds first ancestor of a given kind.
fn first_ancestor_of_kind(&self, mut node: SyntaxNode, kind: SyntaxKind) -> Option<SyntaxNode> {
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<T> LsSyntaxGroup for T where T: Upcast<dyn ParserGroup> + ?Sized {}
18 changes: 15 additions & 3 deletions crates/cairo-lang-language-server/src/lang/inspect/defs.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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)?,
})),
}
}
}
Expand Down Expand Up @@ -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<String> {
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)
Expand Down
52 changes: 49 additions & 3 deletions crates/cairo-lang-language-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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};
Expand Down Expand Up @@ -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<MemberId> {
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.
Expand Down Expand Up @@ -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())
Expand Down
86 changes: 86 additions & 0 deletions crates/cairo-lang-language-server/tests/e2e/goto.rs
Original file line number Diff line number Diff line change
@@ -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<String, String>,
_args: &OrderedHashMap<String, String>,
) -> 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::<lsp_request!("textDocument/definition")>(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)
}
1 change: 1 addition & 0 deletions crates/cairo-lang-language-server/tests/e2e/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod analysis;
mod code_actions;
mod goto;
mod hover;
mod semantic_tokens;
mod support;
Expand Down
Loading