Skip to content

Commit

Permalink
LS: add support for struct members (#6145)
Browse files Browse the repository at this point in the history
  • Loading branch information
piotmag769 authored Aug 6, 2024
1 parent f28ab51 commit aa3865e
Show file tree
Hide file tree
Showing 12 changed files with 365 additions and 28 deletions.
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,6 +1,7 @@
mod analysis;
mod code_actions;
mod completions;
mod goto;
mod hover;
mod semantic_tokens;
mod support;
Expand Down
Loading

0 comments on commit aa3865e

Please sign in to comment.