From 9bf33550de1f854bdc1f1cc01c78c0750bf08c16 Mon Sep 17 00:00:00 2001 From: usagi32 Date: Fri, 25 Oct 2024 03:14:27 +0530 Subject: [PATCH] feat: trait autocompletion --- components/clarity-lsp/src/common/backend.rs | 2 +- .../src/common/requests/capabilities.rs | 5 +- .../src/common/requests/completion.rs | 1608 ++++++++++++++++- .../src/common/requests/helpers.rs | 2 +- components/clarity-lsp/src/common/state.rs | 195 +- .../src/analysis/ast_dependency_detector.rs | 10 +- 6 files changed, 1729 insertions(+), 93 deletions(-) diff --git a/components/clarity-lsp/src/common/backend.rs b/components/clarity-lsp/src/common/backend.rs index a1b1ebd39..548347dfe 100644 --- a/components/clarity-lsp/src/common/backend.rs +++ b/components/clarity-lsp/src/common/backend.rs @@ -286,7 +286,7 @@ pub fn process_request( }; let completion_items = match editor_state - .try_read(|es| es.get_completion_items_for_contract(&contract_location, &position)) + .try_read(|es| es.get_completion_items_for_contract(&contract_location, &position, ¶ms.context)) { Ok(result) => result, Err(_) => return Ok(LspRequestResponse::CompletionItems(vec![])), diff --git a/components/clarity-lsp/src/common/requests/capabilities.rs b/components/clarity-lsp/src/common/requests/capabilities.rs index f04e32bb8..9741684ef 100644 --- a/components/clarity-lsp/src/common/requests/capabilities.rs +++ b/components/clarity-lsp/src/common/requests/capabilities.rs @@ -43,7 +43,10 @@ pub fn get_capabilities(initialization_options: &InitializationOptions) -> Serve }, )), completion_provider: match initialization_options.completion { - true => Some(CompletionOptions::default()), + true => Some(CompletionOptions { + trigger_characters: Some(vec!["'".to_string(),".".to_string(), "<".to_string()]), + ..Default::default() + }), false => None, }, hover_provider: match initialization_options.hover { diff --git a/components/clarity-lsp/src/common/requests/completion.rs b/components/clarity-lsp/src/common/requests/completion.rs index 092212ff1..4d43cb6c0 100644 --- a/components/clarity-lsp/src/common/requests/completion.rs +++ b/components/clarity-lsp/src/common/requests/completion.rs @@ -1,24 +1,32 @@ -use std::{collections::HashMap, vec}; +use std::{collections::{BTreeMap, HashMap, HashSet}, vec}; +use clarinet_files::FileLocation; use clarity_repl::{ analysis::ast_visitor::{traverse, ASTVisitor, TypedVar}, clarity::{ - analysis::ContractAnalysis, - docs::{make_api_reference, make_define_reference, make_keyword_reference}, - functions::{define::DefineFunctions, NativeFunctions}, - variables::NativeVariables, - vm::types::{BlockInfoProperty, FunctionType, TypeSignature}, - ClarityName, ClarityVersion, SymbolicExpression, + analysis::ContractAnalysis, + docs::{make_api_reference, make_define_reference, make_keyword_reference}, + functions::{define::DefineFunctions, NativeFunctions}, + representations::Span, + variables::NativeVariables, + vm::types::{ + signatures::{MethodSignature, MethodType}, BlockInfoProperty, + FunctionType, PrincipalData, QualifiedContractIdentifier, + StandardPrincipalData, TraitIdentifier, TypeSignature + }, + ClarityName, ClarityVersion, StacksEpochId, SymbolicExpression }, - repl::DEFAULT_EPOCH, + repl::{DEFAULT_CLARITY_VERSION, DEFAULT_EPOCH}, }; use lazy_static::lazy_static; use lsp_types::{ - CompletionItem, CompletionItemKind, Documentation, InsertTextFormat, MarkupContent, MarkupKind, - Position, + Command, CompletionContext, CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionTextEdit, + Documentation, InsertTextFormat, MarkupContent, MarkupKind, Position, Range, TextEdit }; use regex::Regex; +use crate::state::{ActiveContractData, ProtocolState}; + use super::helpers::{get_function_at_position, is_position_within_span}; lazy_static! { @@ -76,22 +84,78 @@ lazy_static! { build_fold_valid_cb_completion_items(ClarityVersion::Clarity3); } -#[derive(Clone, Debug, Default)] -pub struct ContractDefinedData { - position: Position, +#[derive(Clone, Debug)] +pub enum DefineFunctionType { + FixedFunction{expects_type: bool}, + UseTrait, + ImplTrait, + None +} + +impl Default for DefineFunctionType { + fn default() -> Self { + Self::None + } +} + +#[derive(Clone, Debug)] +pub struct ContractDefinedData<'a> { + epoch: StacksEpochId, + clarity_version: ClarityVersion, + pub position: Position, consts: Vec<(String, String)>, locals: Vec<(String, String)>, pub vars: Vec, pub maps: Vec, pub fts: Vec, pub nfts: Vec, + pub function_at_position: DefineFunctionType, + pub public_functions: HashMap<&'a ClarityName, Vec>, + pub read_only_functions: HashMap<&'a ClarityName, Vec>, + pub defined_traits: BTreeMap<&'a ClarityName, BTreeMap>, + pub referenced_traits: HashMap<&'a ClarityName, TraitIdentifier>, + pub referenced_traits_span: Option<(u32, u32)>, + pub implemented_traits: HashSet, + pub implemented_traits_span: Option<(u32, u32)>, pub functions_completion_items: Vec, } -impl<'a> ContractDefinedData { - pub fn new(expressions: &[SymbolicExpression], position: &Position) -> Self { +impl<'a> Default for ContractDefinedData<'a> { + fn default() -> Self { + Self { + epoch: DEFAULT_EPOCH, + clarity_version: DEFAULT_CLARITY_VERSION, + position: Default::default(), + consts: Default::default(), + locals: Default::default(), + vars: Default::default(), + maps: Default::default(), + fts: Default::default(), + nfts: Default::default(), + function_at_position: Default::default(), + public_functions: Default::default(), + read_only_functions: Default::default(), + defined_traits: Default::default(), + referenced_traits: Default::default(), + referenced_traits_span: Default::default(), + implemented_traits: Default::default(), + implemented_traits_span: Default::default(), + functions_completion_items: Default::default() + } + } +} + +impl<'a> ContractDefinedData<'a> { + pub fn new( + expressions: &'a [SymbolicExpression], + position: Position, + epoch: StacksEpochId, + clarity_version: ClarityVersion + ) -> Self { let mut defined_data = ContractDefinedData { - position: *position, + position, + epoch, + clarity_version, ..Default::default() }; traverse(&mut defined_data, expressions); @@ -188,7 +252,7 @@ impl<'a> ContractDefinedData { } } -impl<'a> ASTVisitor<'a> for ContractDefinedData { +impl<'a> ASTVisitor<'a> for ContractDefinedData<'a> { fn visit_define_constant( &mut self, _expr: &'a SymbolicExpression, @@ -248,7 +312,33 @@ impl<'a> ASTVisitor<'a> for ContractDefinedData { parameters: Option>>, _body: &'a SymbolicExpression, ) -> bool { - self.set_function_completion_with_bindings(expr, name, ¶meters.unwrap_or_default()); + match &expr.expr { + clarity_repl::clarity::SymbolicExpressionType::List(list) => { + let (_, args) = list.split_first().unwrap(); + let signature = args[0].match_list().unwrap(); + match signature.len() { + 0 | 1 => {}, + _ => { + if is_position_within_span(&zero_to_one_based(&self.position), expr.span(), 0u32){ + self.function_at_position = DefineFunctionType::FixedFunction { + expects_type: check_type_expects(&self.position, &signature[1..]) + } + } + }, + } + }, + _ => {}, + } + + let parameters = parameters.unwrap_or_default(); + let mut args = Vec::new(); + for parameter in ¶meters { + if let Ok(arg) = TypeSignature::parse_type_repr(self.epoch, parameter.type_expr, &mut ()) { + args.push(arg); + } + } + self.set_function_completion_with_bindings(expr, name, ¶meters); + self.public_functions.insert(name, args); true } @@ -259,7 +349,33 @@ impl<'a> ASTVisitor<'a> for ContractDefinedData { parameters: Option>>, _body: &'a SymbolicExpression, ) -> bool { - self.set_function_completion_with_bindings(expr, name, ¶meters.unwrap_or_default()); + match &expr.expr { + clarity_repl::clarity::SymbolicExpressionType::List(list) => { + let (_, args) = list.split_first().unwrap(); + let signature = args[0].match_list().unwrap(); + match signature.len() { + 0 | 1 => {}, + _ => { + if is_position_within_span(&zero_to_one_based(&self.position), expr.span(), 0u32){ + self.function_at_position = DefineFunctionType::FixedFunction { + expects_type: check_type_expects(&self.position, &signature[1..]) + } + } + }, + } + }, + _ => {}, + } + + let parameters = parameters.unwrap_or_default(); + let mut args = Vec::new(); + for parameter in ¶meters { + if let Ok(arg) = TypeSignature::parse_type_repr(self.epoch, parameter.type_expr, &mut ()) { + args.push(arg); + } + } + self.set_function_completion_with_bindings(expr, name, ¶meters); + self.read_only_functions.insert(name, args); true } @@ -270,6 +386,24 @@ impl<'a> ASTVisitor<'a> for ContractDefinedData { parameters: Option>>, _body: &'a SymbolicExpression, ) -> bool { + match &expr.expr { + clarity_repl::clarity::SymbolicExpressionType::List(list) => { + let (_, args) = list.split_first().unwrap(); + let signature = args[0].match_list().unwrap(); + match signature.len() { + 0 | 1 => {}, + _ => { + if is_position_within_span(&zero_to_one_based(&self.position), expr.span(), 0u32){ + self.function_at_position = DefineFunctionType::FixedFunction { + expects_type: check_type_expects(&self.position, &signature[1..]) + } + } + }, + } + }, + _ => {}, + } + self.set_function_completion_with_bindings(expr, name, ¶meters.unwrap_or_default()); true } @@ -280,13 +414,98 @@ impl<'a> ASTVisitor<'a> for ContractDefinedData { bindings: &HashMap<&'a ClarityName, &'a SymbolicExpression>, _body: &'a [SymbolicExpression], ) -> bool { - if is_position_within_span(&self.position, &expr.span, 0) { + if is_position_within_span(&zero_to_one_based(&self.position), &expr.span, 0) { for (name, value) in bindings { self.locals.push((name.to_string(), value.to_string())); } } true } + + fn visit_define_trait( + &mut self, + _expr: &'a SymbolicExpression, + name: &'a ClarityName, + functions: &'a [SymbolicExpression], + ) -> bool { + if let Ok(trait_signature) = TypeSignature::parse_trait_type_repr( + functions, + &mut (), + self.epoch, + self.clarity_version + ) { + self.defined_traits.insert(name, trait_signature); + } + true + } + + fn visit_use_trait( + &mut self, + expr: &'a SymbolicExpression, + name: &'a ClarityName, + trait_identifier: &TraitIdentifier, + ) -> bool { + self.referenced_traits.insert(name, trait_identifier.clone()); + + if let Some((_, e)) = &mut self.referenced_traits_span { + *e = expr.span.end_line; + } else { + self.referenced_traits_span = Some((expr.span.start_line, expr.span.end_line)); + } + + if is_position_within_span(&zero_to_one_based(&self.position), expr.span(), 0u32) { + self.function_at_position = DefineFunctionType::UseTrait; + } + + true + } + + fn visit_impl_trait( + &mut self, + expr: &'a SymbolicExpression, + trait_identifier: &TraitIdentifier, + ) -> bool { + self.implemented_traits.insert(trait_identifier.clone()); + + if let Some((_, e)) = &mut self.referenced_traits_span { + *e = expr.span.end_line; + } else { + self.referenced_traits_span = Some((expr.span.start_line, expr.span.end_line)); + } + + if is_position_within_span(&zero_to_one_based(&self.position), expr.span(), 0u32) + && self.position.character >= 12 { + self.function_at_position = DefineFunctionType::ImplTrait; + } + + true + } +} + +fn zero_to_one_based(position: &Position) -> Position { + Position::new(position.line+1, position.character+1) +} + +fn check_type_expects(position: &Position, list: &[SymbolicExpression]) -> bool { + let pos = zero_to_one_based(position); + for pair_list in list { + if let Some(pair) = pair_list.match_list() { + if 1 == pair.len() + && pos.line >= pair[0].span.end_line + && pos.character > pair[0].span.end_column + && is_position_within_span(&pos, pair_list.span(), 0u32) + { + return true + } + + if 2 == pair.len() + && is_position_within_span(&pos, pair[1].span(), pair_list.span().end_column - pair[1].span().end_column) + { + return true + } + } + } + false } fn build_contract_calls_args(signature: &FunctionType) -> (Vec, Vec) { @@ -339,11 +558,547 @@ pub fn get_contract_calls(analysis: &ContractAnalysis) -> Vec { inter_contract } +pub fn build_trait_completion_data( + issuer: &StandardPrincipalData, + contract_uri: &FileLocation, + contract_defined_state: &ContractDefinedData, + protocol_state: &ProtocolState, + active_contract: &ActiveContractData, + position: &Position, + context: &Option, +) -> Option> { + if let Some(Some(ch)) = &context.to_owned().map(|ctx| ctx.trigger_character) { + 'it: { if let DefineFunctionType::FixedFunction { expects_type: true} = &contract_defined_state.function_at_position + { + if "<" != &ch[0..1] {break 'it} + return Some( + get_trait_alias_completion_data( + contract_uri, + issuer, + contract_defined_state, + protocol_state + ) + ) + } + } + + if "'" == &ch[0..1] { + return Some(get_principal_completion_data(protocol_state)); + } + + if "." == &ch[0..1] { + let precursor_token = active_contract.get_token_at_postion(position).unwrap(); + match precursor_token.as_str() { + "." => return Some(get_contract_name_completion(issuer, protocol_state)), + + token if '\'' == token.chars().nth(0).unwrap() => { + if let Ok(principal) = PrincipalData::parse(&token[..token.len()-1]) { + match principal { + PrincipalData::Standard(principal) => return Some( + get_contract_name_completion( + &principal, + protocol_state + ) + ), + + PrincipalData::Contract(contract) => return Some( + get_trait_name_completion( + contract_uri, + contract_defined_state, + protocol_state, + contract, + position + ) + ), + } + } + }, + + token if '.' == token.chars().nth(0).unwrap() => { + let Ok(name) = token[1..token.len()-1].to_owned().try_into() else {return Some(vec![])}; + let contract = QualifiedContractIdentifier::new( + issuer.clone(), + name + ); + return Some( + get_trait_name_completion( + contract_uri, + contract_defined_state, + protocol_state, + contract, + position + ) + ) + }, + + _ => {}, + } + + return Some(vec![]) + } + } + + let precursor_token = active_contract.get_token_at_postion(position)?; + let sub_tokens = precursor_token.split('.').collect::>(); + let principal = PrincipalData::parse(&sub_tokens[..sub_tokens.len()-1].join(".")); + match (sub_tokens.len(), precursor_token.chars().next().unwrap()) { + (2, '\'') | (3, '\'') if principal.is_ok() => { + match principal.unwrap() { + PrincipalData::Standard(standard_principal_data) => { + return Some(get_contract_name_completion(&standard_principal_data, protocol_state)) + }, + PrincipalData::Contract(qualified_contract_identifier) => { + return Some( + get_trait_name_completion( + contract_uri, + contract_defined_state, + protocol_state, + qualified_contract_identifier, + position + ) + ); + }, + } + }, + + (1, '\'') => return Some(get_principal_completion_data(protocol_state)), + + (2, '.') => return Some(get_contract_name_completion(issuer, protocol_state)), + + (3, '.') => { + let Ok(name) = sub_tokens[1].to_owned().try_into() else {return Some(vec![])}; + let contract = QualifiedContractIdentifier::new( + issuer.clone(), + name + ); + return Some( + get_trait_name_completion( + contract_uri, + contract_defined_state, + protocol_state, + contract, + position + ) + ); + } + + (1, '<') => { + if let DefineFunctionType::FixedFunction { expects_type: true} = &contract_defined_state.function_at_position { + return Some( + get_trait_alias_completion_data( + contract_uri, + issuer, + contract_defined_state, + protocol_state + ) + ) + } + }, + + (1, _) => {}, + (_, _) => return Some(vec![]), + } + + None +} + +fn get_use_trait_suggestions( + pos: &Position, + param: Option, + contract_uri: &FileLocation, + issuer: &StandardPrincipalData, + contract_defined_state: &ContractDefinedData, + protocol_state: &ProtocolState, +) -> Vec { + let mut list = Vec::new(); + for (ident, _) in protocol_state.get_trait_definitions(contract_uri) { + let (label, label_details, insert_text, additional_text_edits) = match (param, *issuer == ident.0.issuer) { + (Some(0), true) => { + let label = format!("{}", ident.1); + let label_details = CompletionItemLabelDetails{ + detail: Some(format!(" .{}.{}", ident.0.name, ident.1)), + description: None, + }; + let insert_text = Some(format!("(use-trait {} .{}.{})", ident.1, ident.0.name, ident.1)); + let additional_text_edits = Some(vec![TextEdit::new(Range::new(Position::new(pos.line, 0), Position::new(pos.line, 999)), "".to_owned())]); + + (label, Some(label_details), insert_text, additional_text_edits) + }, + + (Some(0), false) => { + let label = format!("{}", ident.1); + let address = ident.0.issuer.to_address(); + let shorthand_address = format!("'{}..{}", &address[..3], &address[address.len()-3..address.len()]); + let shorthand_trait_identifier = format!(" {}.{}.{}", shorthand_address, ident.0.name, ident.1); + let label_details = CompletionItemLabelDetails{ + detail: Some(shorthand_trait_identifier), + description: None, + }; + + let trait_identifier = format!("{}.{}.{}", address, ident.0.name, ident.1); + let insert_text = Some(format!("(use-trait {} '{})", ident.1, trait_identifier)); + let additional_text_edits = Some(vec![TextEdit::new(Range::new(Position::new(pos.line, 0), Position::new(pos.line, 999)), "".to_owned())]); + + (label, Some(label_details), insert_text, additional_text_edits) + }, + + (Some(1), true) => { + let label = format!(".{}.{}", ident.0.name, ident.1); + let insert_text = format!(".{}.{}",ident.0.name, ident.1); + + (label, None, Some(insert_text), None) + }, + + (Some(1), false) => { + let address = ident.0.issuer.to_address(); + let shorthand_address = format!("'{}..{}", &address[..3], &address[address.len()-3..address.len()]); + let shorthand_trait_identifier = format!("{}.{}.{}", shorthand_address, ident.0.name, ident.1); + let label = shorthand_trait_identifier; + + let trait_identifier = format!("{}.{}.{}", address, ident.0.name, ident.1); + let insert_text = format!("'{}", trait_identifier); + + (label, None, Some(insert_text), None) + }, + + (_, _) => return vec![] + }; + + list.push(CompletionItem{ + label, + label_details, + insert_text, + additional_text_edits, + ..Default::default() + }) + } + list +} + +fn get_trait_alias_completion_data( + contract_uri: &FileLocation, + issuer: &StandardPrincipalData, + contract_defined_state: &ContractDefinedData, + protocol_state: &ProtocolState, +) -> Vec { + + let mut list = Vec::new(); + + for trait_alias in contract_defined_state.referenced_traits.keys() + { + list.push(CompletionItem { + label: trait_alias.to_string(), + kind: Some(CompletionItemKind::INTERFACE), + detail: Some("trait-alias".to_string()), + insert_text: Some(format!("{}>", trait_alias)), + ..Default::default() + }); + } + + for (trait_identity, _) in protocol_state.get_trait_definitions(contract_uri) { + if contract_defined_state.referenced_traits.contains_key(&trait_identity.1) {continue;} + + let (principal, contract) = match &trait_identity.0 { + x if *issuer == x.issuer => ("".to_string(), &x.name), + + x => { + (x.issuer.to_address(), &x.name) + }, + }; + + let (insert_line, extra_lines) = if let Some((s, _)) = contract_defined_state.referenced_traits_span { + (s-1, "\n".to_owned()) + } else { + (0, "\n\n".to_owned()) + }; + + let insert_postion = Position::new(insert_line, 0u32); + + let detail = if !principal.is_empty(){ + let shorthand_address = format!("'{}..{}", &principal[..3], &principal[principal.len()-3..principal.len()]); + Some(format!(" use-trait {} {}.{}.{}", trait_identity.1, shorthand_address, contract, trait_identity.1)) + } else { + Some(format!(" use-trait {} .{}.{}", trait_identity.1, contract, trait_identity.1)) + }; + + let new_text = if principal.is_empty() { + format!("(use-trait {} .{}.{}){}", trait_identity.1, contract, trait_identity.1, extra_lines) + } else { + format!("(use-trait {} '{}.{}.{}){}", trait_identity.1, principal, contract, trait_identity.1, extra_lines) + }; + + list.push(CompletionItem { + label: trait_identity.1.to_string(), + label_details: Some(CompletionItemLabelDetails { + detail, + description: None, + }), + kind: Some(CompletionItemKind::INTERFACE), + detail: Some("trait-alias".to_string()), + insert_text: Some(format!("{}>", trait_identity.1)), + additional_text_edits: Some(vec![TextEdit { + range: Range { start: insert_postion, end: insert_postion}, + new_text, + }]), + ..Default::default() + }); + } + + list +} + +fn get_principal_completion_data(protocol_state: &ProtocolState) -> Vec { + let mut set = HashSet::new(); + for address in protocol_state + .get_contract_identifiers() + .iter() + .map(|x|x.issuer.to_address()) + { + set.insert(address); + } + set.into_iter().map(|x| CompletionItem::new_simple(x, "".to_string())).collect::>() +} + +fn get_contract_name_completion( + principal: &StandardPrincipalData, + protocol_state: &ProtocolState, +) -> Vec { + let mut list = Vec::new(); + for contract in protocol_state.get_contract_identifiers() { + if *principal == contract.issuer { + list.push(CompletionItem::new_simple(contract.name.to_string(), "".to_string())); + } + } + + list +} + +fn get_trait_name_completion( + contract_uri: &FileLocation, + contract_defined_state: &ContractDefinedData, + protocol_state: &ProtocolState, + contract: QualifiedContractIdentifier, + pos: &Position, +) -> Vec { + let mut list = Vec::new(); + + match contract_defined_state.function_at_position { + DefineFunctionType::ImplTrait => { + for signature in protocol_state + .get_trait_definitions(contract_uri) + .iter() + .filter(|x| contract == x.0.0) + { + let mut methods = String::new(); + let mut methods_are_some = false; + methods.push_str("\n\n"); + + for method in signature.1 { + match &method.1.define_type { + MethodType::ReadOnly => { + if !(contract_defined_state + .read_only_functions + .get(&method.0) + .is_some_and(|x| *x==method.1.args)) + { + methods_are_some = true; + let mut params = String::new(); + + for (i, x) in method.1.args.iter().enumerate() { + params.push_str(&format!(" (param{}-name {})", i+1, x)[..]); + } + + methods.push_str(format!("(define-read-only ({}{}) body)\n\n", method.0, params).as_str()); + } + }, + + MethodType::Public => { + if !(contract_defined_state + .public_functions + .get(&method.0) + .is_some_and(|x| *x==method.1.args)) + { + methods_are_some = true; + let mut params = String::new(); + + for (i, x) in method.1.args.iter().enumerate() { + params.push_str(&format!(" (param{}-name {})", i+1, x)[..]); + } + + methods.push_str(format!("(define-public ({}{}) body)\n\n", method.0, params).as_str()); + } + }, + + MethodType::NotDefined => { + if !(contract_defined_state + .read_only_functions + .get(&method.0) + .is_some_and(|x| *x==method.1.args)) + && + !(contract_defined_state + .public_functions + .get(&method.0) + .is_some_and(|x| *x==method.1.args)) + { + methods_are_some = true; + let mut params = String::new(); + + for (i, x) in method.1.args.iter().enumerate() { + params.push_str(&format!(" (param{}-name {})", i+1, x)[..]); + } + + methods.push_str(format!("(access-modifier-kind ({}{}) body)\n\n", method.0, params).as_str()); + } + }, + } + } + + let position = Position::new(pos.line, 999); + let methods = match methods_are_some { + true => Some(vec![TextEdit::new(lsp_types::Range { start: position, end: position }, methods)]), + false => None, + }; + + list.push(CompletionItem{ + label: signature.0.1.to_string(), + additional_text_edits: methods, + ..Default::default() + }) + } + }, + + _ => { + for definition in protocol_state + .get_trait_definitions(contract_uri) + .iter() + .filter(|x| contract == x.0.0) + { + list.push(CompletionItem::new_simple(definition.0.1.to_string(), "".to_string())); + } + } + } + + list +} + +pub fn get_impl_trait_suggestions( + pos: &Position, + contract_uri: &FileLocation, + issuer: StandardPrincipalData, + contract_defined_state: &ContractDefinedData, + protocol_state: &ProtocolState, +) -> Vec { + let mut list = Vec::new(); + + for signature in protocol_state.get_trait_definitions(contract_uri) { + if contract_defined_state + .implemented_traits + .contains(&TraitIdentifier::new( + signature.0.0.issuer.clone(), + signature.0.0.name.clone(), + signature.0.1.clone())) + { + continue; + } + + let mut methods_to_insert = String::new(); + methods_to_insert.push_str("\n\n"); + for method in signature.1 { + match method.1.define_type { + MethodType::ReadOnly => { + if !(contract_defined_state + .read_only_functions + .get(&method.0) + .is_some_and(|x| *x == method.1.args)) + { + let mut params = String::new(); + + for (i, x) in method.1.args.iter().enumerate() { + params.push_str(&format!(" (param{}-name {})", i+1, x)[..]); + } + + methods_to_insert.push_str(format!("(define-read-only ({}{}) body)\n\n", method.0, params).as_str()); + } + }, + + MethodType::Public => { + if !(contract_defined_state + .public_functions + .get(&method.0) + .is_some_and(|x| *x == method.1.args)) + { + let mut params = String::new(); + + for (i, x) in method.1.args.iter().enumerate() { + params.push_str(&format!(" (param{}-name {})", i+1, x)[..]); + } + + methods_to_insert.push_str(format!("(define-public ({}{}) body)\n\n", method.0, params).as_str()); + } + }, + + MethodType::NotDefined => { + if !(contract_defined_state + .read_only_functions + .get(&method.0) + .is_some_and(|x| *x==method.1.args)) + && + !(contract_defined_state + .public_functions + .get(&method.0) + .is_some_and(|x| *x==method.1.args)) + { + let mut params = String::new(); + + for (i, x) in method.1.args.iter().enumerate() { + params.push_str(&format!(" (param{}-name {})", i+1, x)[..]); + } + + methods_to_insert.push_str(format!("(access-modifier-kind ({}{}) body)\n\n", method.0, params).as_str()); + } + }, + } + } + + let additional_text_edits = match methods_to_insert.len() { + 2 => None, + _ => Some(vec![(TextEdit::new( + Range::new(Position::new(pos.line, 999), Position::new(pos.line, 999)), + methods_to_insert + ))]) + }; + + let shorthand_address = format!("{}..{}", + &signature.0.0.issuer.to_address()[..3], + &signature.0.0.issuer.to_address()[signature.0.0.issuer.to_address().len()-3..]); + let label = format!("{}.{}.{}",shorthand_address, signature.0.0.name, signature.0.1); + + let insert_text = if issuer != signature.0.0.issuer { + Some(format!("'{}.{}.{}", signature.0.0.issuer.to_address(), signature.0.0.name, signature.0.1)) + } else { + Some(format!(".{}.{}", signature.0.0.name, signature.0.1)) + }; + + list.push(CompletionItem{ + label, + insert_text, + additional_text_edits, + ..Default::default() + }); + + } + + list +} + pub fn build_completion_item_list( clarity_version: &ClarityVersion, expressions: &Vec, + contract_uri: &FileLocation, position: &Position, + current_contract_issuer: Option, active_contract_defined_data: &ContractDefinedData, + protocol_state: &ProtocolState, contract_calls: Vec, should_wrap: bool, include_native_placeholders: bool, @@ -351,14 +1106,43 @@ pub fn build_completion_item_list( if let Some((function_name, param)) = get_function_at_position(position, expressions) { // - for var-*, map-*, ft-* or nft-* methods, return the corresponding data names let mut completion_strings: Option> = None; - if VAR_FUNCTIONS.contains(&function_name.to_string()) && param == Some(0) { - completion_strings = Some(active_contract_defined_data.vars.clone()); - } else if MAP_FUNCTIONS.contains(&function_name.to_string()) && param == Some(0) { - completion_strings = Some(active_contract_defined_data.maps.clone()); - } else if FT_FUNCTIONS.contains(&function_name.to_string()) && param == Some(0) { - completion_strings = Some(active_contract_defined_data.fts.clone()); - } else if NFT_FUNCTIONS.contains(&function_name.to_string()) && param == Some(0) { - completion_strings = Some(active_contract_defined_data.nfts.clone()); + match (function_name.to_string(), param) { + (name, Some(0)) if VAR_FUNCTIONS.contains(&name) => completion_strings = Some(active_contract_defined_data.vars.clone()), + + (name, Some(0)) if MAP_FUNCTIONS.contains(&name) => completion_strings = Some(active_contract_defined_data.maps.clone()), + + (name, Some(0)) if FT_FUNCTIONS.contains(&name) => completion_strings = Some(active_contract_defined_data.fts.clone()), + + (name, Some(0)) if NFT_FUNCTIONS.contains(&name) => completion_strings = Some(active_contract_defined_data.nfts.clone()), + + (name, Some(0)) if name == *"impl-trait" && current_contract_issuer.is_some() => { + let issuer = current_contract_issuer.unwrap(); + return get_impl_trait_suggestions( + &Position::new(position.line-1, position.character-1), + contract_uri, + issuer, + active_contract_defined_data, + protocol_state + ) + }, + + (name, _) if name == *"impl-trait" => return vec![], + + (name, param) if name == *"use-trait" && current_contract_issuer.is_some() => { + let issuer = current_contract_issuer.unwrap(); + return get_use_trait_suggestions( + &Position::new(position.line-1, position.character-1), + param, + contract_uri, + &issuer, + active_contract_defined_data, + protocol_state + ) + } + + (name, _) if name == *"use-trait" => return vec![], + + (_, _) => {} } if let Some(completion_strings) = completion_strings { @@ -472,6 +1256,22 @@ pub fn build_completion_item_list( _ => {} } + if *"impl-trait" == item.label { + item.command = Some(Command::new( + "triggerSuggest".into(), + "editor.action.triggerSuggest".into(), + None + )); + } + + if *"use-trait" == item.label { + item.command = Some(Command::new( + "triggerSuggest".into(), + "editor.action.triggerSuggest".into(), + None + )); + } + completion_items.push(item); } completion_items @@ -786,15 +1586,15 @@ fn get_iterator_cb_completion_item(version: &ClarityVersion, func: &str) -> Vec< #[cfg(test)] mod get_contract_global_data_tests { - use clarity_repl::clarity::ast::build_ast_with_rules; + use clarity_repl::clarity::ast::{build_ast_with_rules, ContractAST}; use clarity_repl::clarity::vm::types::QualifiedContractIdentifier; use clarity_repl::clarity::{ClarityVersion, StacksEpochId}; use lsp_types::Position; use super::ContractDefinedData; - fn get_defined_data(source: &str) -> ContractDefinedData { - let contract_ast = build_ast_with_rules( + fn get_ast(source: &str) -> ContractAST { + build_ast_with_rules( &QualifiedContractIdentifier::transient(), source, &mut (), @@ -802,48 +1602,71 @@ mod get_contract_global_data_tests { StacksEpochId::Epoch21, clarity_repl::clarity::ast::ASTRules::Typical, ) - .unwrap(); - ContractDefinedData::new(&contract_ast.expressions, &Position::default()) + .unwrap() } #[test] fn get_data_vars() { - let data = get_defined_data( + let contract_ast = get_ast( "(define-data-var counter uint u1) (define-data-var is-active bool true)", ); + let data = ContractDefinedData::new( + &contract_ast.expressions, + Position::default(), + StacksEpochId::Epoch21, + ClarityVersion::Clarity2 + ); assert_eq!(data.vars, ["counter", "is-active"]); } #[test] fn get_map() { - let data = get_defined_data("(define-map names principal { name: (buff 48) })"); + let contract_ast = get_ast("(define-map names principal { name: (buff 48) })"); + let data = ContractDefinedData::new( + &contract_ast.expressions, + Position::default(), + StacksEpochId::Epoch21, + ClarityVersion::Clarity2 + ); assert_eq!(data.maps, ["names"]); } #[test] fn get_fts() { - let data = get_defined_data("(define-fungible-token clarity-coin)"); + let contract_ast = get_ast("(define-fungible-token clarity-coin)"); + let data = ContractDefinedData::new( + &contract_ast.expressions, + Position::default(), + StacksEpochId::Epoch21, + ClarityVersion::Clarity2 + ); assert_eq!(data.fts, ["clarity-coin"]); } #[test] fn get_nfts() { - let data = get_defined_data("(define-non-fungible-token bitcoin-nft uint)"); + let contract_ast = get_ast("(define-non-fungible-token bitcoin-nft uint)"); + let data = ContractDefinedData::new( + &contract_ast.expressions, + Position::default(), + StacksEpochId::Epoch21, + ClarityVersion::Clarity2 + ); assert_eq!(data.nfts, ["bitcoin-nft"]); } } #[cfg(test)] mod get_contract_local_data_tests { - use clarity_repl::clarity::ast::build_ast_with_rules; + use clarity_repl::clarity::ast::{build_ast_with_rules, ContractAST}; use clarity_repl::clarity::StacksEpochId; use clarity_repl::clarity::{vm::types::QualifiedContractIdentifier, ClarityVersion}; use lsp_types::Position; use super::ContractDefinedData; - fn get_defined_data(source: &str, position: &Position) -> ContractDefinedData { - let contract_ast = build_ast_with_rules( + fn get_ast(source: &str) -> ContractAST { + build_ast_with_rules( &QualifiedContractIdentifier::transient(), source, &mut (), @@ -851,38 +1674,52 @@ mod get_contract_local_data_tests { StacksEpochId::Epoch21, clarity_repl::clarity::ast::ASTRules::Typical, ) - .unwrap(); - ContractDefinedData::new(&contract_ast.expressions, position) + .unwrap() } #[test] fn get_function_binding() { - let data = get_defined_data( + let contract_ast = get_ast( "(define-private (print-arg (arg int)) )", - &Position { + ); + let data = ContractDefinedData::new( + &contract_ast.expressions, + Position { line: 1, character: 38, }, + StacksEpochId::Epoch21, + ClarityVersion::Clarity2 ); assert_eq!(data.locals, vec![("arg".to_string(), "int".to_string())]); - let data = get_defined_data( + let contract_ast = get_ast( "(define-private (print-arg (arg int)) )", - &Position { + ); + let data = ContractDefinedData::new( + &contract_ast.expressions, + Position { line: 1, character: 40, }, + StacksEpochId::Epoch21, + ClarityVersion::Clarity2 ); assert_eq!(data.locals, vec![]); } #[test] fn get_let_binding() { - let data = get_defined_data( + let contract_ast = get_ast( "(let ((n u0)) )", - &Position { + ); + let data = ContractDefinedData::new( + &contract_ast.expressions, + Position { line: 1, character: 15, }, + StacksEpochId::Epoch21, + ClarityVersion::Clarity2 ); assert_eq!(data.locals, vec![("n".to_string(), "u0".to_string())]); } @@ -897,7 +1734,7 @@ mod populate_snippet_with_options_tests { use super::ContractDefinedData; - fn get_defined_data(source: &str) -> ContractDefinedData { +/* fn get_defined_data(source: &str) -> ContractDefinedData { let contract_ast = build_ast_with_rules( &QualifiedContractIdentifier::transient(), source, @@ -907,13 +1744,27 @@ mod populate_snippet_with_options_tests { clarity_repl::clarity::ast::ASTRules::Typical, ) .unwrap(); - ContractDefinedData::new(&contract_ast.expressions, &Position::default()) - } + ContractDefinedData::new(&contract_ast.expressions, &Position::default(), StacksEpochId::Epoch21, ClarityVersion::Clarity2) + } */ #[test] fn get_data_vars_snippet() { - let data = get_defined_data( - "(define-data-var counter uint u1) (define-data-var is-active bool true)", + let source = + "(define-data-var counter uint u1) (define-data-var is-active bool true)"; + let contract_ast = build_ast_with_rules( + &QualifiedContractIdentifier::transient(), + source, + &mut (), + ClarityVersion::Clarity2, + StacksEpochId::Epoch21, + clarity_repl::clarity::ast::ASTRules::Typical, + ) + .unwrap(); + let data = ContractDefinedData::new( + &contract_ast.expressions, + Position::default(), + StacksEpochId::Epoch21, + ClarityVersion::Clarity2 ); let snippet = data.populate_snippet_with_options( &ClarityVersion::Clarity2, @@ -925,7 +1776,22 @@ mod populate_snippet_with_options_tests { #[test] fn get_map_snippet() { - let data = get_defined_data("(define-map names principal { name: (buff 48) })"); + let source = "(define-map names principal { name: (buff 48) })"; + let contract_ast = build_ast_with_rules( + &QualifiedContractIdentifier::transient(), + source, + &mut (), + ClarityVersion::Clarity2, + StacksEpochId::Epoch21, + clarity_repl::clarity::ast::ASTRules::Typical, + ) + .unwrap(); + let data = ContractDefinedData::new( + &contract_ast.expressions, + Position::default(), + StacksEpochId::Epoch21, + ClarityVersion::Clarity2 + ); let snippet = data.populate_snippet_with_options( &ClarityVersion::Clarity2, &"map-get?".to_string(), @@ -939,7 +1805,22 @@ mod populate_snippet_with_options_tests { #[test] fn get_fts_snippet() { - let data = get_defined_data("(define-fungible-token btc u21)"); + let source = "(define-fungible-token btc u21)"; + let contract_ast = build_ast_with_rules( + &QualifiedContractIdentifier::transient(), + source, + &mut (), + ClarityVersion::Clarity2, + StacksEpochId::Epoch21, + clarity_repl::clarity::ast::ASTRules::Typical, + ) + .unwrap(); + let data = ContractDefinedData::new( + &contract_ast.expressions, + Position::default(), + StacksEpochId::Epoch21, + ClarityVersion::Clarity2 + ); let snippet = data.populate_snippet_with_options( &ClarityVersion::Clarity2, &"ft-mint?".to_string(), @@ -953,7 +1834,22 @@ mod populate_snippet_with_options_tests { #[test] fn get_nfts_snippet() { - let data = get_defined_data("(define-non-fungible-token bitcoin-nft uint)"); + let source = "(define-non-fungible-token bitcoin-nft uint)"; + let contract_ast = build_ast_with_rules( + &QualifiedContractIdentifier::transient(), + source, + &mut (), + ClarityVersion::Clarity2, + StacksEpochId::Epoch21, + clarity_repl::clarity::ast::ASTRules::Typical, + ) + .unwrap(); + let data = ContractDefinedData::new( + &contract_ast.expressions, + Position::default(), + StacksEpochId::Epoch21, + ClarityVersion::Clarity2 + ); let snippet = data.populate_snippet_with_options( &ClarityVersion::Clarity2, &"nft-mint?".to_string(), @@ -965,3 +1861,603 @@ mod populate_snippet_with_options_tests { ); } } + +mod trait_tests { + use std::{cmp::Ordering, collections::{BTreeMap, HashMap}, vec}; + + use clarinet_files::FileLocation; + use clarity_repl::{ + analysis::ast_visitor::{traverse, ASTVisitor}, + clarity::{analysis::ContractAnalysis, + ast::{build_ast_with_diagnostics, build_ast_with_rules, parser, ASTRules, ContractAST}, + costs::LimitedCostTracker, + vm::types::{PrincipalData, QualifiedContractIdentifier, StandardPrincipalData, TypeSignature}}, + repl::{DEFAULT_CLARITY_VERSION, DEFAULT_EPOCH} + }; + use lsp_types::{CompletionContext, CompletionItem, CompletionTriggerKind, Position, Range}; + + use crate::{ + common::requests::{completion::ContractDefinedData, + helpers::is_position_within_span}, + state::{ActiveContractData, ContractState, ProtocolState} + }; + + use super::{build_completion_item_list, build_trait_completion_data, DefineFunctionType}; + + #[derive(Debug, Eq, PartialEq, Copy, Clone, Default)] + struct RangeExoSkeleton { + inner: Range, + } + + impl From for RangeExoSkeleton { + fn from(value: Range) -> Self { + Self { inner: value } + } + } + + impl Ord for RangeExoSkeleton { + fn cmp(&self, other: &Self) -> Ordering { + // for now, this approximation is enough + self.inner.start.line.cmp(&other.inner.start.line) + .then_with(|| self.inner.end.line.cmp(&other.inner.end.line)) + .then_with(|| self.inner.end.character.cmp(&other.inner.end.character)) + } + } + + impl PartialOrd for RangeExoSkeleton { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + fn replace_range_in_place(text: &mut String, mut start_line: usize, mut start_char: usize, mut end_line: usize, mut end_char: usize, replacement: &str) { + let lines: Vec<&str> = text.lines().collect(); + + if start_line >= lines.len() { + start_line = lines.len().saturating_sub(1); + } + + if end_line >= lines.len() { + end_line = lines.len().saturating_sub(1); + } + + let start_line_chars: Vec<(usize, char)> = lines.get(start_line).map(|x| x.char_indices().collect()).unwrap_or_default(); + if start_char > start_line_chars.len() { + start_char = start_line_chars.len() + } + + let end_line_chars: Vec<(usize, char)> = lines.get(end_line).map(|x| x.char_indices().collect()).unwrap_or_default(); + if end_char > end_line_chars.len() { + end_char = end_line_chars.len() + } + + let mut start_byte_offset = 0; + let mut end_byte_offset = 0; + let mut current_offset = 0; + + for (i, line) in lines.iter().enumerate() { + if i < start_line { + current_offset += line.len()+1; + } else if i == start_line { + start_byte_offset = current_offset + start_char; + break; + } + } + + current_offset = 0; + + for (i, line) in lines.iter().enumerate() { + if i < end_line { + current_offset += line.len()+1; + } else if i == end_line { + end_byte_offset = current_offset + end_char; + break; + } + } + + text.replace_range(start_byte_offset..end_byte_offset, replacement); + } + + // what is being done here is trying to emulate how a CompletionItem is applied to a raw text file. + // According to language server protocol documentaion if a CompletionItem contains n TextEdits + // they are applied from bottom to the top of text document. And it makes sense intuitively + // to do so beacuse the changes will mimic as if they are applied to the original document. + // This function simulates this process according to the specific use cases and possibilities. + fn apply_completion_item(source: &mut String, word: &str, pos: &Position, item: &CompletionItem) { + // A sorted map. Since overlapping edits are prohibited, a simple comparison will suffice. + let mut map: Vec<(RangeExoSkeleton, String)> = Vec::new(); + + if let Some(text) = &item.insert_text { + map.push((Range::new(*pos, *pos).into(), text.replacen(word, "", 1))); + } else { + map.push((Range::new(*pos, *pos).into(), item.label.replacen(word, "", 1))) + } + + if let Some(edits) = &item.additional_text_edits { + for edit in edits { + map.push((edit.range.into(), edit.new_text.clone())); + } + } + + map.sort_by(|a, b| a.0.cmp(&b.0)); + + while let Some((RangeExoSkeleton { inner }, text)) = map.pop() { + replace_range_in_place( + source, + inner.start.line as usize, + inner.start.character as usize, + inner.end.line as usize, + inner.end.character as usize, + &text[..] + ); + } + } + + #[derive(Debug)] + struct AnalysisExoSkeleton<'a> { + inner: &'a mut ContractAnalysis, + } + + impl<'a> ASTVisitor<'a> for AnalysisExoSkeleton<'a> { + fn visit_define_trait( + &mut self, + expr: &'a clarity_repl::clarity::SymbolicExpression, + name: &'a clarity_repl::clarity::ClarityName, + functions: &'a [clarity_repl::clarity::SymbolicExpression], + ) -> bool { + let function_types = TypeSignature::parse_trait_type_repr( + functions, + &mut (), + DEFAULT_EPOCH, + DEFAULT_CLARITY_VERSION + ).unwrap(); + self.inner.add_defined_trait(name.clone(), function_types); + true + } + } + + fn build_protocol_state() -> ProtocolState { + let contract1_principal = "ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE"; + let contract1_name = "timelocked-wallet"; + let contract1_source = r#" +(define-trait locked-wallet-trait + ( + (public lock (principal uint uint) (response bool uint)) + (read-only bestow (principal) (response bool uint)) + (claim () (response bool uint)) + ) +) +"#; + + let contract2_principal = "ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK"; + let contract2_name = "abc-contract"; + let contract2_source = r#" +(define-trait abc + ( + (public a (principal uint uint) (response bool uint)) + (read-only b (principal) (response bool uint)) + (c () (response bool uint)) + ) +)"#; + + let mut state = ProtocolState::new(); + add_contracts_to_state( + &mut state, + vec![ + (contract1_principal, contract1_name, contract1_source), + (contract2_principal, contract2_name, contract2_source), + ] + ); + + state + } + + fn get_ast(contract_identifier: &QualifiedContractIdentifier, source_code: &str) -> ContractAST { + build_ast_with_diagnostics(contract_identifier, source_code, &mut (), DEFAULT_CLARITY_VERSION, DEFAULT_EPOCH).0 + } + + fn add_contracts_to_state( + state: &mut ProtocolState, + contracts: Vec<(&str, &str, &str)>, + ) { + let mut locations = HashMap::new(); + let mut asts = BTreeMap::new(); + let mut analyses = HashMap::new(); + let mut clarity_versions = HashMap::new(); + for (contract_principal, contract_name, contract_source) in contracts { + let contract_identifier = QualifiedContractIdentifier::new( + PrincipalData::parse_standard_principal(contract_principal).unwrap(), + contract_name.into() + ); + let contract_ast = get_ast(&contract_identifier, contract_source); + let mut contract_analysis = ContractAnalysis::new( + contract_identifier.clone(), + contract_ast.expressions.clone(), + LimitedCostTracker::Free, + DEFAULT_EPOCH, + DEFAULT_CLARITY_VERSION + ); + + traverse(&mut AnalysisExoSkeleton{ inner: &mut contract_analysis }, &contract_ast.expressions); + analyses.insert(contract_identifier.clone(), Some(contract_analysis)); + asts.insert(contract_identifier.clone(), contract_ast); + clarity_versions.insert(contract_identifier.clone(), DEFAULT_CLARITY_VERSION); + locations.insert(contract_identifier, FileLocation::from_path_string(&format!("/{}.clar", contract_name)).unwrap()); + } + + state.consolidate( + &mut locations, + &mut asts, + &mut BTreeMap::new(), + &mut HashMap::new(), + &mut HashMap::new(), + &mut analyses, + &mut clarity_versions + ); + } + + fn get_active_contract_data(source_code: &str) -> ActiveContractData { + ActiveContractData::new(DEFAULT_CLARITY_VERSION, DEFAULT_EPOCH, None, source_code) + } + + fn get_completion_list( + contract: &str, + issuer: Option, + pos: &Position, + context: &Option + ) -> Option> { + let state = build_protocol_state(); + let active_contract_data = get_active_contract_data(contract); + + build_trait_completion_data( + &issuer.unwrap_or(StandardPrincipalData::transient()), + &FileLocation::from_path_string("/test.clar").unwrap(), + &ContractDefinedData::new( + &active_contract_data.expressions.clone().unwrap_or_default()[..], + *pos, + DEFAULT_EPOCH, + DEFAULT_CLARITY_VERSION + ), + &state, + &active_contract_data, + pos, + context + ) + } + + #[test] + fn test_principal_autocomplete() { + let mut contract = "'".to_string(); + let pos = Position::new(0, 1); + let context = Some(CompletionContext{ + trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER, + trigger_character: Some("'".to_string()) + }); + + let list = get_completion_list(&contract, None, &pos, &context); + + let expected_result = ["'ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE", "'ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK"]; + + assert!(list.is_some_and(|list| + list.len() == 2 + && { + apply_completion_item(&mut contract, "", &pos, &list[0]); + expected_result.contains(&contract.as_str()) + } + )); + } + + #[test] + fn test_incomplete_principal_completion() { + let mut contract = "'ST1J".to_string(); + let pos = Position::new(0, 5); + let context = None; + + let list = get_completion_list(&contract, None, &pos, &context); + + let expected_result = ["'ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE", "'ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK"]; + + assert!(list.is_some_and(|list| + list.len() == 2 + && { + for item in list { + if item.label.contains("ST1J") { + apply_completion_item(&mut contract, "ST1J", &pos, &item); + } + } + expected_result.contains(&contract.as_str()) + } + )); + } + + #[test] + fn test_sugared_contract_name_completion() { + let mut contract = ".".to_string(); + let pos = Position::new(0, 1); + let context = Some(CompletionContext{ + trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER, + trigger_character: Some(".".to_string()) + }); + let issuer = PrincipalData::parse_standard_principal("ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE").unwrap(); + + let list = get_completion_list(&contract, Some(issuer), &pos, &context); + + let expected_result = ".timelocked-wallet"; + + assert!(list.is_some_and(|list| + list.len() == 1 + && { + apply_completion_item(&mut contract, "", &pos, &list[0]); + expected_result == contract + } + )); + } + + #[test] + fn test_sugared_partial_contract_name_completion() { + let mut contract = ".a".to_string(); + let pos = Position::new(0, 2); + let context = None; + let issuer = PrincipalData::parse_standard_principal("ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK").unwrap(); + + let list = get_completion_list(&contract, Some(issuer), &pos, &context); + + let expected_result = ".abc-contract"; + + assert!(list.is_some_and(|list| + list.len() == 1 + && { + apply_completion_item(&mut contract, "a", &pos, &list[0]); + expected_result == contract + } + )); + } + + #[test] + fn test_qualified_contract_name_completion() { + let mut contract = "'ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE.".to_string(); + let pos = Position::new(0, 43); + let context = Some(CompletionContext{ + trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER, + trigger_character: Some(".".to_string()) + }); + + let list = get_completion_list(&contract, None, &pos, &context); + + let expected_result = "'ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE.timelocked-wallet"; + + assert!(list.is_some_and(|list| + list.len() == 1 + && { + apply_completion_item(&mut contract, "", &pos, &list[0]); + expected_result == contract + } + )); + } + + #[test] + fn test_qualified_partial_contract_name_completion() { + let mut contract = "'ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE.time".to_string(); + let pos = Position::new(0, 47); + + let list = get_completion_list(&contract, None, &pos, &None); + + let expected_result = "'ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE.timelocked-wallet"; + + assert!(list.is_some_and(|list| + list.len() == 1 + && { + apply_completion_item(&mut contract, "time", &pos, &list[0]); + expected_result == contract + } + )); + } + + #[test] + fn test_sugared_trait_name() { + let mut contract = ".timelocked-wallet.".to_string(); + let pos = Position::new(0, 19); + let context = Some(CompletionContext{ + trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER, + trigger_character: Some(".".to_string()) + }); + let issuer = PrincipalData::parse_standard_principal("ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE").unwrap(); + + let list = get_completion_list(&contract, Some(issuer), &pos, &context); + + let expected_result = ".timelocked-wallet.locked-wallet-trait"; + + assert!(list.is_some_and(|list| + list.len() == 1 + && { + apply_completion_item(&mut contract, "", &pos, &list[0]); + expected_result == contract + } + )); + } + + #[test] + fn test_qualified_trait_name() { + let mut contract = "'ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK.abc-contract.".to_string(); + let pos = Position::new(0, 56); + let context = Some(CompletionContext{ + trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER, + trigger_character: Some(".".to_string()) + }); + + let list = get_completion_list(&contract, None, &pos, &context); + + let expected_result = "'ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK.abc-contract.abc"; + + assert!(list.is_some_and(|list| + list.len() == 1 + && { + apply_completion_item(&mut contract, "", &pos, &list[0]); + expected_result == contract + } + )); + } + + #[test] + fn test_sugared_partial_trait_name() { + let mut contract = "(impl-trait .abc-contract.a)".to_string(); + let pos = Position::new(0, 27); + let issuer = PrincipalData::parse_standard_principal("ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK").unwrap(); + + let list = get_completion_list(&contract, Some(issuer), &pos, &None); + + let expected_result = "(impl-trait .abc-contract.abc) + +(define-public (a (param1-name principal) (param2-name uint) (param3-name uint)) body) + +(define-read-only (b (param1-name principal)) body) + +(access-modifier-kind (c) body) + +"; + + assert!(list.is_some_and(|list| + list.len() == 1 + && { + apply_completion_item(&mut contract, "a", &pos, &list[0]); + expected_result == contract + } + )); + } + + fn build_completion_list(contract: &str, pos: &Position, issuer: &StandardPrincipalData) -> Vec { + let state = build_protocol_state(); + let active_contract_data = get_active_contract_data(contract); + + build_completion_item_list( + &DEFAULT_CLARITY_VERSION, + &active_contract_data.expressions.clone().unwrap(), + &FileLocation::from_path_string("/test.clar").unwrap(), + &Position::new(pos.line+1, pos.character+1), + Some(issuer.clone()), + &ContractDefinedData::new( + &active_contract_data.expressions.clone().unwrap_or_default()[..], + *pos, + DEFAULT_EPOCH, + DEFAULT_CLARITY_VERSION + ), + &state, + vec![], + false, + false + ) + } + + #[test] + fn test_impl_trait_suggestions() { + let mut contract: String = "(impl-trait ) +some-random-contract".into(); + let pos = Position::new(0, 12); + let issuer = PrincipalData::parse_standard_principal("ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE").unwrap(); + + let list = build_completion_list(&contract, &pos, &issuer); + + for item in list { + if item.label.contains("timelocked-wallet") { + apply_completion_item(&mut contract, "", &pos, &item) + } + } + + let expected_result: String = "(impl-trait .timelocked-wallet.locked-wallet-trait) + +(define-read-only (bestow (param1-name principal)) body) + +(access-modifier-kind (claim) body) + +(define-public (lock (param1-name principal) (param2-name uint) (param3-name uint)) body) + + +some-random-contract".into(); + + assert_eq!(contract, expected_result) + } + + #[test] + fn test_use_trait_suggestions() { + let mut contract = "(use-trait )".to_string(); + let pos = Position::new(0, 11); + let issuer = StandardPrincipalData::transient(); + + let list = build_completion_list(&contract, &pos, &issuer); + + for item in list { + if item.label.contains("abc") { + apply_completion_item(&mut contract, "", &pos, &item) + } + } + + let expected_result = "(use-trait abc 'ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK.abc-contract.abc)".to_owned(); + + assert_eq!(contract, expected_result); + } + + #[test] + fn test_use_trait_suggestions2() { + let mut contract = "(use-trait abc )".to_string(); + let pos = Position::new(0, 15); + let issuer = PrincipalData::parse_standard_principal("ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK").unwrap(); + + let list = build_completion_list(&contract, &pos, &issuer); + + for item in list { + if item.label.contains("abc") { + apply_completion_item(&mut contract, "", &pos, &item) + } + } + + let expected_result = "(use-trait abc .abc-contract.abc)".to_owned(); + + assert_eq!(contract, expected_result); + } + + #[test] + fn test_trait_alias_completion() { + let mut contract = "(define-public (set (n )) body)"; + + assert_eq!(contract, expected_result); + } +} diff --git a/components/clarity-lsp/src/common/requests/helpers.rs b/components/clarity-lsp/src/common/requests/helpers.rs index c8a10b625..07216b9aa 100644 --- a/components/clarity-lsp/src/common/requests/helpers.rs +++ b/components/clarity-lsp/src/common/requests/helpers.rs @@ -16,7 +16,7 @@ pub fn span_to_range(span: &Span) -> Range { } } -// end_offset is usded to include the end position of a keyword, for go to definition in particular +// end_offset is used to include the end position of a keyword, for go to definition in particular pub fn is_position_within_span(position: &Position, span: &Span, end_offset: u32) -> bool { if position.line < span.start_line || position.line > span.end_line { return false; diff --git a/components/clarity-lsp/src/common/state.rs b/components/clarity-lsp/src/common/state.rs index f626719ba..0ff6a9c64 100644 --- a/components/clarity-lsp/src/common/state.rs +++ b/components/clarity-lsp/src/common/state.rs @@ -3,29 +3,29 @@ use clarinet_deployments::{ generate_default_deployment, initiate_session_from_manifest, update_session_with_deployment_plan, UpdateSessionExecutionResult, }; -use clarinet_files::ProjectManifest; +use clarinet_files::{NetworkManifest, ProjectManifest}; use clarinet_files::StacksNetwork; use clarinet_files::{FileAccessor, FileLocation}; use clarity_repl::analysis::ast_dependency_detector::DependencySet; use clarity_repl::clarity::analysis::ContractAnalysis; -use clarity_repl::clarity::ast::{build_ast_with_rules, ASTRules}; +use clarity_repl::clarity::ast::{build_ast_with_diagnostics, build_ast_with_rules, ASTRules}; use clarity_repl::clarity::diagnostic::{Diagnostic as ClarityDiagnostic, Level as ClarityLevel}; +use clarity_repl::clarity::vm::types::signatures::MethodSignature; use clarity_repl::clarity::vm::ast::ContractAST; -use clarity_repl::clarity::vm::types::{QualifiedContractIdentifier, StandardPrincipalData}; +use clarity_repl::clarity::vm::types::{QualifiedContractIdentifier, StandardPrincipalData, PrincipalData}; use clarity_repl::clarity::vm::EvaluationResult; use clarity_repl::clarity::{ClarityName, ClarityVersion, StacksEpochId, SymbolicExpression}; use clarity_repl::repl::{ContractDeployer, DEFAULT_CLARITY_VERSION}; use lsp_types::{ - CompletionItem, DocumentSymbol, Hover, Location, MessageType, Position, Range, SignatureHelp, - Url, + CompletionContext, CompletionItem, DocumentSymbol, Hover, Location, MessageType, Position, Range, SignatureHelp, Url }; -use std::borrow::BorrowMut; +use std::borrow::{BorrowMut, Cow}; use std::collections::{BTreeMap, HashMap, HashSet}; use std::vec; use super::requests::capabilities::InitializationOptions; use super::requests::completion::{ - build_completion_item_list, get_contract_calls, ContractDefinedData, + build_completion_item_list, build_trait_completion_data, get_contract_calls, ContractDefinedData }; use super::requests::definitions::{ get_definitions, get_public_function_definitions, DefinitionLocation, @@ -53,15 +53,16 @@ impl ActiveContractData { issuer: Option, source: &str, ) -> Self { - match build_ast_with_rules( - &QualifiedContractIdentifier::transient(), - source, - &mut (), - clarity_version, - epoch, - ASTRules::PrecheckSize, - ) { - Ok(ast) => ActiveContractData { + let (ast, diagnostics, success) = build_ast_with_diagnostics( + &QualifiedContractIdentifier::transient(), + source, + &mut (), + clarity_version, + epoch + ); + + if success { + ActiveContractData { clarity_version, epoch, issuer: issuer.clone(), @@ -69,16 +70,25 @@ impl ActiveContractData { definitions: Some(get_definitions(&ast.expressions, issuer)), diagnostic: None, source: source.to_string(), - }, - Err(err) => ActiveContractData { + } + } else { + ActiveContractData { clarity_version, epoch, - issuer, - expressions: None, - definitions: None, - diagnostic: Some(err.diagnostic), + issuer: issuer.clone(), + expressions: if !ast.expressions.is_empty() { + Some(ast.expressions.clone()) + } else { + None + }, + definitions: if !ast.expressions.is_empty() { + Some(get_definitions(&ast.expressions, issuer)) + } else { + None + }, + diagnostic: Some(diagnostics.first().unwrap().clone()), source: source.to_string(), - }, + } } } @@ -122,6 +132,29 @@ impl ActiveContractData { self.issuer = issuer; self.update_definitions(); } + + // maybe better than persisting state of Vec to get placeholder values. + pub fn get_token_at_postion(&self, pos: &Position,) -> Option { + let lines = self.source.lines().collect::>(); + + if pos.line < lines.len() as u32 { + let line = lines[pos.line as usize]; + + if pos.character as usize <= line.len() { + let trimmed = &line[..pos.character as usize]; + + let words: Vec<&str> = trimmed.split_whitespace().collect(); + return words.last().map(|s| s.to_string()) + } + } + + None + } + + pub fn get_last_line(&self) -> u32 { + let lines = self.source.lines().collect::>(); + (lines.len()+1) as u32 + } } #[derive(Debug, Clone, PartialEq)] @@ -296,22 +329,98 @@ impl EditorState { &self, contract_location: &FileLocation, position: &Position, + context: &Option, ) -> Vec { let active_contract = match self.active_contracts.get(contract_location) { Some(contract) => contract, None => return vec![], }; - let contract_calls = self + let (state, contract_calls) = self .contracts_lookup .get(contract_location) .and_then(|d| self.protocols.get(&d.manifest_location)) - .map(|p| p.get_contract_calls_for_contract(contract_location)) - .unwrap_or_default(); + .map(|p| (Cow::Borrowed(p), p.get_contract_calls_for_contract(contract_location))) + .unwrap_or((Cow::Owned(ProtocolState::new()), vec![])); + + let expressions = active_contract + .expressions + .as_ref() + .map(Cow::Borrowed) + .unwrap_or(Cow::Owned(vec![])); - let expressions = active_contract.expressions.as_ref(); let active_contract_defined_data = - ContractDefinedData::new(expressions.unwrap_or(&vec![]), position); + ContractDefinedData::new( + &expressions[..], + *position, + active_contract.epoch, + active_contract.clarity_version + ); + + let issuer = 'it: { + let Some((network_manifest, deployer)) = self.contracts_lookup + .get(contract_location) + .and_then(|d| ProjectManifest::from_location(&d.manifest_location).ok()) + .and_then(|manifest| + NetworkManifest::from_project_manifest_location( + &manifest.location, + &StacksNetwork::Simnet.get_networks(), + Some(&manifest.project.cache_location), + Option::None + ) + .ok() + .and_then(|network_manifest| + manifest + .contracts_settings + .get(contract_location) + .map(|metadata| (network_manifest, metadata.deployer.clone())) + ) + ) else { + break 'it None + }; + + let issuer = match &deployer { + ContractDeployer::DefaultDeployer => { + let default_deployer = match network_manifest.accounts.get("deployer") { + Some(deployer) => deployer, + None => break 'it None, + }; + + match PrincipalData::parse_standard_principal(&default_deployer.stx_address) { + Ok(res) => res, + Err(_) => break 'it None, + } + }, + + ContractDeployer::LabeledDeployer(deployer) => { + let deployer = match network_manifest.accounts.get(deployer) { + Some(deployer) => deployer, + None => break 'it None, + }; + match PrincipalData::parse_standard_principal(&deployer.stx_address) { + Ok(res) => res, + Err(_) => break 'it None, + } + }, + + _ => {break 'it None}, + }; + + if let Some(trait_completion_data) = build_trait_completion_data( + &issuer, + contract_location, + &active_contract_defined_data, + &state, + active_contract, + position, + context + ) { + return trait_completion_data + } + + Some(issuer) + }; + let should_wrap = match self.settings.completion_smart_parenthesis_wrap { true => check_if_should_wrap(&active_contract.source, position), false => true, @@ -319,12 +428,15 @@ impl EditorState { build_completion_item_list( &active_contract.clarity_version, - expressions.unwrap_or(&vec![]), + &expressions, + contract_location, &Position { line: position.line + 1, character: position.character + 1, }, + issuer, &active_contract_defined_data, + &state, contract_calls, should_wrap, self.settings.completion_include_native_placeholders, @@ -616,6 +728,31 @@ impl ProtocolState { } contract_calls } + + pub fn get_trait_definitions( + &self, + contract_uri: &FileLocation + ) -> BTreeMap<(QualifiedContractIdentifier, ClarityName), BTreeMap> { + let mut traits = BTreeMap::new(); + for (url, contract_state) in self.contracts.iter() { + if !contract_uri.eq(url) { + if let Some(analysis) = &contract_state.analysis { + for (name, signature) in analysis.defined_traits.iter() { + traits.insert((contract_state.contract_id.clone(), name.to_owned()), signature.to_owned()); + } + } + } + } + traits + } + + pub fn get_contract_identifiers(&self) -> Vec { + let mut contract_ids = Vec::new(); + for (_, state) in self.contracts.iter() { + contract_ids.push(state.contract_id.clone()); + } + contract_ids + } } pub async fn build_state( diff --git a/components/clarity-repl/src/analysis/ast_dependency_detector.rs b/components/clarity-repl/src/analysis/ast_dependency_detector.rs index 42b0f6304..2cf639719 100644 --- a/components/clarity-repl/src/analysis/ast_dependency_detector.rs +++ b/components/clarity-repl/src/analysis/ast_dependency_detector.rs @@ -7,9 +7,9 @@ pub use clarity::vm::analysis::types::ContractAnalysis; use clarity::vm::analysis::{CheckErrors, CheckResult}; use clarity::vm::ast::ContractAST; use clarity::vm::representations::{SymbolicExpression, TraitDefinition}; -use clarity::vm::types::signatures::CallableSubtype; +use clarity::vm::types::signatures::{CallableSubtype, MethodSignature}; use clarity::vm::types::{ - FunctionSignature, PrincipalData, QualifiedContractIdentifier, SequenceSubtype, + PrincipalData, QualifiedContractIdentifier, SequenceSubtype, TraitIdentifier, TypeSignature, Value, }; use clarity::vm::{ClarityName, ClarityVersion, SymbolicExpressionType}; @@ -31,7 +31,7 @@ pub struct ASTDependencyDetector<'a> { BTreeMap<(&'a QualifiedContractIdentifier, &'a ClarityName), Vec>, defined_traits: BTreeMap< (&'a QualifiedContractIdentifier, &'a ClarityName), - BTreeMap, + BTreeMap, >, defined_contract_constants: BTreeMap< (&'a QualifiedContractIdentifier, &'a ClarityName), @@ -383,7 +383,7 @@ impl<'a> ASTDependencyDetector<'a> { &mut self, contract_identifier: &'a QualifiedContractIdentifier, name: &'a ClarityName, - trait_definition: BTreeMap, + trait_definition: BTreeMap, ) { if let Some(pending) = self.pending_trait_checks.remove(&TraitIdentifier { name: name.clone(), @@ -441,7 +441,7 @@ impl<'a> ASTDependencyDetector<'a> { fn check_trait_dependencies( &self, - trait_definition: &BTreeMap, + trait_definition: &BTreeMap, function_name: &ClarityName, args: &'a [SymbolicExpression], ) -> BTreeSet {