diff --git a/components/clarity-lsp/src/common/requests/capabilities.rs b/components/clarity-lsp/src/common/requests/capabilities.rs index b3a418d90..a9e5f8701 100644 --- a/components/clarity-lsp/src/common/requests/capabilities.rs +++ b/components/clarity-lsp/src/common/requests/capabilities.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; pub struct InitializationOptions { completion: bool, pub completion_smart_parenthesis_wrap: bool, - pub completion_include_params_in_snippet: bool, + pub completion_include_native_placeholders: bool, document_symbols: bool, go_to_definition: bool, hover: bool, diff --git a/components/clarity-lsp/src/common/requests/completion.rs b/components/clarity-lsp/src/common/requests/completion.rs index 0fb8c3b84..ca0dfe23a 100644 --- a/components/clarity-lsp/src/common/requests/completion.rs +++ b/components/clarity-lsp/src/common/requests/completion.rs @@ -12,6 +12,10 @@ use lsp_types::{ }; lazy_static! { + static ref COMPLETION_ITEMS_CLARITY_1: Vec = + build_default_native_keywords_list(ClarityVersion::Clarity1); + static ref COMPLETION_ITEMS_CLARITY_2: Vec = + build_default_native_keywords_list(ClarityVersion::Clarity2); static ref VAR_FUNCTIONS: Vec = vec![ NativeFunctions::SetVar.to_string(), NativeFunctions::FetchVar.to_string(), @@ -35,10 +39,6 @@ lazy_static! { NativeFunctions::MintAsset.to_string(), NativeFunctions::TransferAsset.to_string(), ]; - pub static ref COMPLETION_ITEMS_CLARITY_1: Vec = - build_default_native_keywords_list(ClarityVersion::Clarity1); - pub static ref COMPLETION_ITEMS_CLARITY_2: Vec = - build_default_native_keywords_list(ClarityVersion::Clarity2); } #[derive(Clone, Debug, Default)] @@ -47,182 +47,160 @@ pub struct ContractDefinedData { pub maps: Vec, pub fts: Vec, pub nfts: Vec, + pub consts: Vec<(String, String)>, } -pub fn get_contract_defined_data( - expressions: Option<&Vec>, -) -> Option { - let mut defined_data = ContractDefinedData { - ..Default::default() - }; - - for expression in expressions? { - let (define_function, args) = expression.match_list()?.split_first()?; - match DefineFunctions::lookup_by_name(define_function.match_atom()?)? { - DefineFunctions::PersistedVariable => defined_data - .vars - .push(args.first()?.match_atom()?.to_string()), - DefineFunctions::Map => defined_data - .maps - .push(args.first()?.match_atom()?.to_string()), - DefineFunctions::FungibleToken => defined_data - .fts - .push(args.first()?.match_atom()?.to_string()), - DefineFunctions::NonFungibleToken => defined_data - .nfts - .push(args.first()?.match_atom()?.to_string()), - _ => (), +impl ContractDefinedData { + pub fn new(expressions: &Vec) -> Self { + let mut defined_data = ContractDefinedData::default(); + for expression in expressions { + expression + .match_list() + .and_then(|list| list.split_first()) + .and_then(|(function_name, args)| { + Some(( + DefineFunctions::lookup_by_name(function_name.match_atom()?), + args.first()?.match_atom()?.to_string(), + args, + )) + }) + .and_then(|(define_function, name, args)| { + match define_function { + Some(DefineFunctions::PersistedVariable) => defined_data.vars.push(name), + Some(DefineFunctions::Map) => defined_data.maps.push(name), + Some(DefineFunctions::FungibleToken) => defined_data.fts.push(name), + Some(DefineFunctions::NonFungibleToken) => defined_data.nfts.push(name), + Some(DefineFunctions::Constant) => { + defined_data.consts.push((name, args.last()?.to_string())) + } + _ => (), + }; + Some(()) + }); } - } - Some(defined_data) -} - -#[cfg(test)] -mod get_contract_defined_data_tests { - use clarity_repl::clarity::ast::build_ast_with_rules; - use clarity_repl::clarity::stacks_common::types::StacksEpochId; - use clarity_repl::clarity::{vm::types::QualifiedContractIdentifier, ClarityVersion}; - - use super::{get_contract_defined_data, ContractDefinedData}; - - fn get_defined_data(source: &str) -> Option { - let contract_ast = build_ast_with_rules( - &QualifiedContractIdentifier::transient(), - source, - &mut (), - ClarityVersion::Clarity2, - StacksEpochId::Epoch21, - clarity_repl::clarity::ast::ASTRules::Typical, - ) - .unwrap(); - get_contract_defined_data(Some(&contract_ast.expressions)) + defined_data } - #[test] - fn get_data_vars() { - let data = get_defined_data( - "(define-data-var counter uint u1) (define-data-var is-active bool true)", - ) - .unwrap_or_default(); - assert_eq!(data.vars, ["counter", "is-active"]); - } - - #[test] - fn get_map() { - let data = get_defined_data("(define-map names principal { name: (buff 48) })") - .unwrap_or_default(); - assert_eq!(data.maps, ["names"]); - } - - #[test] - fn get_fts() { - let data = get_defined_data("(define-fungible-token clarity-coin)").unwrap_or_default(); - assert_eq!(data.fts, ["clarity-coin"]); - } - - #[test] - fn get_nfts() { - let data = - get_defined_data("(define-non-fungible-token bitcoin-nft uint)").unwrap_or_default(); - assert_eq!(data.nfts, ["bitcoin-nft"]); + pub fn populate_snippet_with_options(&self, name: &String, snippet: &String) -> Option { + if VAR_FUNCTIONS.contains(name) && self.vars.len() > 0 { + let choices = self.vars.join(","); + return Some(snippet.replace("${1:var}", &format!("${{1|{:}|}}", choices))); + } + if MAP_FUNCTIONS.contains(name) && self.maps.len() > 0 { + let choices = self.maps.join(","); + return Some(snippet.replace("${1:map-name}", &format!("${{1|{:}|}}", choices))); + } + if FT_FUNCTIONS.contains(name) && self.fts.len() > 0 { + let choices = self.fts.join(","); + return Some(snippet.replace("${1:token-name}", &format!("${{1|{:}|}}", choices))); + } + if NFT_FUNCTIONS.contains(name) && self.nfts.len() > 0 { + let choices = self.nfts.join(","); + return Some(snippet.replace("${1:asset-name}", &format!("${{1|{:}|}}", choices))); + } + None } -} -pub fn populate_snippet_with_options( - name: &String, - snippet: &String, - defined_data: &ContractDefinedData, -) -> String { - if VAR_FUNCTIONS.contains(name) && defined_data.vars.len() > 0 { - let choices = defined_data.vars.join(","); - return snippet.replace("${1:var}", &format!("${{1|{:}|}}", choices)); - } else if MAP_FUNCTIONS.contains(name) && defined_data.maps.len() > 0 { - let choices = defined_data.maps.join(","); - return snippet.replace("${1:map-name}", &format!("${{1|{:}|}}", choices)); - } else if FT_FUNCTIONS.contains(name) && defined_data.fts.len() > 0 { - let choices = defined_data.fts.join(","); - return snippet.replace("${1:token-name}", &format!("${{1|{:}|}}", choices)); - } else if NFT_FUNCTIONS.contains(name) && defined_data.nfts.len() > 0 { - let choices = defined_data.nfts.join(","); - return snippet.replace("${1:asset-name}", &format!("${{1|{:}|}}", choices)); + pub fn get_consts_completion_item(&self) -> Vec { + self.consts + .iter() + .map(|(name, definition)| { + CompletionItem::new_simple(name.to_string(), definition.to_string()) + }) + .collect() } - return snippet.to_string(); } -#[cfg(test)] -mod populate_snippet_with_options_tests { - use clarity_repl::clarity::ast::build_ast_with_rules; - use clarity_repl::clarity::stacks_common::types::StacksEpochId; - use clarity_repl::clarity::{vm::types::QualifiedContractIdentifier, ClarityVersion}; - - use super::{get_contract_defined_data, populate_snippet_with_options, ContractDefinedData}; - - fn get_defined_data(source: &str) -> Option { - let contract_ast = build_ast_with_rules( - &QualifiedContractIdentifier::transient(), - source, - &mut (), - ClarityVersion::Clarity2, - StacksEpochId::Epoch21, - clarity_repl::clarity::ast::ASTRules::Typical, - ) - .unwrap(); - get_contract_defined_data(Some(&contract_ast.expressions)) - } - - #[test] - fn get_data_vars_snippet() { - let data = get_defined_data( - "(define-data-var counter uint u1) (define-data-var is-active bool true)", - ) - .unwrap_or_default(); - - let snippet = populate_snippet_with_options( - &"var-get".to_string(), - &"var-get ${1:var}".to_string(), - &data, - ); - assert_eq!(snippet, "var-get ${1|counter,is-active|}"); - } - - #[test] - fn get_map_snippet() { - let data = get_defined_data("(define-map names principal { name: (buff 48) })") - .unwrap_or_default(); - - let snippet = populate_snippet_with_options( - &"map-get?".to_string(), - &"map-get? ${1:map-name} ${2:key-tuple}".to_string(), - &data, - ); - assert_eq!(snippet, "map-get? ${1|names|} ${2:key-tuple}"); - } +pub fn build_completion_item_list( + contract_defined_data: &ContractDefinedData, + clarity_version: &ClarityVersion, + user_defined_keywords: Vec, + should_wrap: bool, + include_native_placeholders: bool, +) -> Vec { + let native_keywords = match clarity_version { + ClarityVersion::Clarity1 => COMPLETION_ITEMS_CLARITY_1.to_vec(), + ClarityVersion::Clarity2 => COMPLETION_ITEMS_CLARITY_2.to_vec(), + }; + let mut completion_items = vec![]; + completion_items.append(&mut contract_defined_data.get_consts_completion_item()); + for mut item in [native_keywords, user_defined_keywords].concat().drain(..) { + match item.kind { + Some( + CompletionItemKind::EVENT + | CompletionItemKind::FUNCTION + | CompletionItemKind::MODULE + | CompletionItemKind::CLASS, + ) => { + let mut snippet = item.insert_text.take().unwrap(); + let mut snippet_has_choices = false; + if item.kind == Some(CompletionItemKind::FUNCTION) { + if let Some(populated_snippet) = + contract_defined_data.populate_snippet_with_options(&item.label, &snippet) + { + snippet_has_choices = true; + snippet = populated_snippet; + } + } + if !include_native_placeholders + && !snippet_has_choices + && (item.kind == Some(CompletionItemKind::FUNCTION) + || item.kind == Some(CompletionItemKind::CLASS)) + { + match item.label.as_str() { + "+ (add)" => { + snippet = "+".to_string(); + } + "- (subtract)" => { + snippet = "-".to_string(); + } + "/ (divide)" => { + snippet = "/".to_string(); + } + "* (multiply)" => { + snippet = "*".to_string(); + } + "< (less than)" => { + snippet = "<".to_string(); + } + "<= (less than or equal)" => { + snippet = "<=".to_string(); + } + "> (greater than)" => { + snippet = ">".to_string(); + } + ">= (greater than or equal)" => { + snippet = ">=".to_string(); + } + _ => snippet = item.label.clone(), + } + snippet.push_str(" $0"); + } - #[test] - fn get_fts_snippet() { - let data = get_defined_data("(define-fungible-token btc u21)").unwrap_or_default(); - let snippet = populate_snippet_with_options( - &"ft-mint?".to_string(), - &"ft-mint? ${1:token-name} ${2:amount} ${3:recipient}".to_string(), - &data, - ); - assert_eq!(snippet, "ft-mint? ${1|btc|} ${2:amount} ${3:recipient}"); - } + item.insert_text = if should_wrap { + Some(format!("({})", snippet)) + } else { + Some(snippet) + }; + } + Some(CompletionItemKind::TYPE_PARAMETER) => { + if should_wrap { + match item.label.as_str() { + "tuple" | "buff" | "string-ascii" | "string-utf8" | "optional" + | "response" | "principal" => { + item.insert_text = Some(format!("({} $0)", item.label)); + item.insert_text_format = Some(InsertTextFormat::SNIPPET); + } + _ => (), + } + } + } + _ => {} + } - #[test] - fn get_nfts_snippet() { - let data = - get_defined_data("(define-non-fungible-token bitcoin-nft uint)").unwrap_or_default(); - let snippet = populate_snippet_with_options( - &"nft-mint?".to_string(), - &"nft-mint? ${1:asset-name} ${2:asset-identifier} ${3:recipient}".to_string(), - &data, - ); - assert_eq!( - snippet, - "nft-mint? ${1|bitcoin-nft|} ${2:asset-identifier} ${3:recipient}" - ); + completion_items.push(item); } + completion_items } pub fn check_if_should_wrap(source: &str, position: &Position) -> bool { @@ -250,6 +228,12 @@ pub fn build_default_native_keywords_list(version: ClarityVersion) -> Vec = vec![NativeFunctions::ElementAt, NativeFunctions::IndexOf]; + let command = lsp_types::Command { + title: "triggerParameterHints".into(), + command: "editor.action.triggerParameterHints".into(), + arguments: None, + }; + let native_functions: Vec = NativeFunctions::ALL .iter() .filter_map(|func| { @@ -275,6 +259,7 @@ pub fn build_default_native_keywords_list(version: ClarityVersion) -> Vec Vec Vec Vec>() } + +#[cfg(test)] +mod get_contract_defined_data_tests { + use clarity_repl::clarity::ast::build_ast_with_rules; + use clarity_repl::clarity::stacks_common::types::StacksEpochId; + use clarity_repl::clarity::{vm::types::QualifiedContractIdentifier, ClarityVersion}; + + use super::ContractDefinedData; + + fn get_defined_data(source: &str) -> ContractDefinedData { + let contract_ast = build_ast_with_rules( + &QualifiedContractIdentifier::transient(), + source, + &mut (), + ClarityVersion::Clarity2, + StacksEpochId::Epoch21, + clarity_repl::clarity::ast::ASTRules::Typical, + ) + .unwrap(); + ContractDefinedData::new(&contract_ast.expressions) + } + + #[test] + fn get_data_vars() { + let data = get_defined_data( + "(define-data-var counter uint u1) (define-data-var is-active bool true)", + ); + assert_eq!(data.vars, ["counter", "is-active"]); + } + + #[test] + fn get_map() { + let data = get_defined_data("(define-map names principal { name: (buff 48) })"); + assert_eq!(data.maps, ["names"]); + } + + #[test] + fn get_fts() { + let data = get_defined_data("(define-fungible-token clarity-coin)"); + assert_eq!(data.fts, ["clarity-coin"]); + } + + #[test] + fn get_nfts() { + let data = get_defined_data("(define-non-fungible-token bitcoin-nft uint)"); + assert_eq!(data.nfts, ["bitcoin-nft"]); + } +} + +#[cfg(test)] +mod populate_snippet_with_options_tests { + use clarity_repl::clarity::ast::build_ast_with_rules; + use clarity_repl::clarity::stacks_common::types::StacksEpochId; + use clarity_repl::clarity::{vm::types::QualifiedContractIdentifier, ClarityVersion}; + + use super::ContractDefinedData; + + fn get_defined_data(source: &str) -> ContractDefinedData { + let contract_ast = build_ast_with_rules( + &QualifiedContractIdentifier::transient(), + source, + &mut (), + ClarityVersion::Clarity2, + StacksEpochId::Epoch21, + clarity_repl::clarity::ast::ASTRules::Typical, + ) + .unwrap(); + ContractDefinedData::new(&contract_ast.expressions) + } + + #[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 snippet = data + .populate_snippet_with_options(&"var-get".to_string(), &"var-get ${1:var}".to_string()); + assert_eq!(snippet, Some("var-get ${1|counter,is-active|}".to_string())); + } + + #[test] + fn get_map_snippet() { + let data = get_defined_data("(define-map names principal { name: (buff 48) })"); + let snippet = data.populate_snippet_with_options( + &"map-get?".to_string(), + &"map-get? ${1:map-name} ${2:key-tuple}".to_string(), + ); + assert_eq!( + snippet, + Some("map-get? ${1|names|} ${2:key-tuple}".to_string()) + ); + } + + #[test] + fn get_fts_snippet() { + let data = get_defined_data("(define-fungible-token btc u21)"); + let snippet = data.populate_snippet_with_options( + &"ft-mint?".to_string(), + &"ft-mint? ${1:token-name} ${2:amount} ${3:recipient}".to_string(), + ); + assert_eq!( + snippet, + Some("ft-mint? ${1|btc|} ${2:amount} ${3:recipient}".to_string()) + ); + } + + #[test] + fn get_nfts_snippet() { + let data = get_defined_data("(define-non-fungible-token bitcoin-nft uint)"); + let snippet = data.populate_snippet_with_options( + &"nft-mint?".to_string(), + &"nft-mint? ${1:asset-name} ${2:asset-identifier} ${3:recipient}".to_string(), + ); + assert_eq!( + snippet, + Some("nft-mint? ${1|bitcoin-nft|} ${2:asset-identifier} ${3:recipient}".to_string()) + ); + } +} diff --git a/components/clarity-lsp/src/common/requests/signature_help.rs b/components/clarity-lsp/src/common/requests/signature_help.rs index 2ce249e5d..279141d17 100644 --- a/components/clarity-lsp/src/common/requests/signature_help.rs +++ b/components/clarity-lsp/src/common/requests/signature_help.rs @@ -77,7 +77,6 @@ pub fn get_signatures( active_parameter = Some(variadic_index.try_into().unwrap()); } } - SignatureInformation { active_parameter, documentation: None, @@ -100,7 +99,6 @@ pub fn get_signatures( #[cfg(test)] mod definitions_visitor_tests { - use clarity_repl::clarity::ClarityVersion::Clarity2; use clarity_repl::clarity::{ functions::NativeFunctions, stacks_common::types::StacksEpochId::Epoch21, @@ -157,9 +155,7 @@ mod definitions_visitor_tests { #[test] fn ensure_all_native_function_have_valid_signature() { - let native_methods = NativeFunctions::ALL_NAMES; - - for method in native_methods { + for method in NativeFunctions::ALL_NAMES { if ["let", "begin"].contains(&method) { continue; } diff --git a/components/clarity-lsp/src/common/state.rs b/components/clarity-lsp/src/common/state.rs index 636cba36a..56b509c83 100644 --- a/components/clarity-lsp/src/common/state.rs +++ b/components/clarity-lsp/src/common/state.rs @@ -19,18 +19,15 @@ use clarity_repl::clarity::vm::EvaluationResult; use clarity_repl::clarity::{ClarityName, ClarityVersion, SymbolicExpression}; use clarity_repl::repl::{ContractDeployer, DEFAULT_CLARITY_VERSION}; use lsp_types::{ - CompletionItem, CompletionItemKind, DocumentSymbol, Hover, Location, MessageType, Position, - Range, SignatureHelp, Url, + CompletionItem, DocumentSymbol, Hover, Location, MessageType, Position, Range, SignatureHelp, + Url, }; use std::borrow::BorrowMut; use std::collections::{BTreeMap, HashMap, HashSet}; use std::vec; use super::requests::capabilities::InitializationOptions; -use super::requests::completion::{ - get_contract_defined_data, populate_snippet_with_options, COMPLETION_ITEMS_CLARITY_1, - COMPLETION_ITEMS_CLARITY_2, -}; +use super::requests::completion::{build_completion_item_list, ContractDefinedData}; use super::requests::definitions::{get_definitions, DefinitionLocation}; use super::requests::document_symbols::ASTSymbols; use super::requests::helpers::{get_atom_start_at_position, get_public_function_definitions}; @@ -299,18 +296,9 @@ impl EditorState { contract_location: &FileLocation, position: &Position, ) -> Vec { - let contract = self.active_contracts.get(&contract_location).unwrap(); - let native_keywords = match contract.clarity_version { - ClarityVersion::Clarity1 => COMPLETION_ITEMS_CLARITY_1.to_vec(), - ClarityVersion::Clarity2 => COMPLETION_ITEMS_CLARITY_2.to_vec(), - }; - - let should_wrap = match self.settings.completion_smart_parenthesis_wrap { - true => match self.active_contracts.get(contract_location) { - Some(active_contract) => check_if_should_wrap(&active_contract.source, position), - None => true, - }, - false => true, + let active_contract = match self.active_contracts.get(&contract_location) { + Some(contract) => contract, + None => return vec![], }; let user_defined_keywords = self @@ -321,51 +309,33 @@ impl EditorState { .unwrap_or_default(); let contract_defined_data = - get_contract_defined_data(contract.expressions.as_ref()).unwrap_or_default(); - - let mut completion_items = vec![]; - for mut item in [native_keywords, user_defined_keywords].concat().drain(..) { - match item.kind { - Some( - CompletionItemKind::EVENT - | CompletionItemKind::FUNCTION - | CompletionItemKind::MODULE - | CompletionItemKind::CLASS, - ) => { - let mut snippet = item.insert_text.take().unwrap(); - if item.kind == Some(CompletionItemKind::FUNCTION) { - snippet = populate_snippet_with_options( - &item.label, - &snippet, - &contract_defined_data, - ); - } + ContractDefinedData::new(active_contract.expressions.as_ref().unwrap_or(&vec![])); - item.insert_text = if should_wrap { - Some(format!("({})", snippet)) - } else { - Some(snippet) - }; - } - _ => {} - } - completion_items.push(item); - } + let should_wrap = match self.settings.completion_smart_parenthesis_wrap { + true => check_if_should_wrap(&active_contract.source, position), + false => true, + }; - completion_items + build_completion_item_list( + &contract_defined_data, + &active_contract.clarity_version, + user_defined_keywords, + should_wrap, + self.settings.completion_include_native_placeholders, + ) } pub fn get_document_symbols_for_contract( &self, contract_location: &FileLocation, ) -> Vec { - let active_contract = self.active_contracts.get(contract_location); + let active_contract = match self.active_contracts.get(&contract_location) { + Some(contract) => contract, + None => return vec![], + }; - let expressions = match active_contract { - Some(active_contract) => match &active_contract.expressions { - Some(expressions) => expressions, - None => return vec![], - }, + let expressions = match &active_contract.expressions { + Some(expressions) => expressions, None => return vec![], }; @@ -400,7 +370,6 @@ impl EditorState { DefinitionLocation::External(contract_identifier, function_name) => { let metadata = self.contracts_lookup.get(contract_location)?; let protocol = self.protocols.get(&metadata.manifest_location)?; - let definition_contract_location = protocol.locations_lookup.get(contract_identifier)?; diff --git a/components/clarity-vscode/client/src/common.ts b/components/clarity-vscode/client/src/common.ts index ce548aa2d..46a9b2494 100644 --- a/components/clarity-vscode/client/src/common.ts +++ b/components/clarity-vscode/client/src/common.ts @@ -64,7 +64,7 @@ export async function initClient( [ "completion", "completionSmartParenthesisWrap", - "completionIncludeParamsInSnippet", + "completionIncludeNativePlaceholders", "hover", "documentSymbols", "goToDefinition", diff --git a/components/clarity-vscode/package.json b/components/clarity-vscode/package.json index 0c0f9e4c5..2975ad0e3 100644 --- a/components/clarity-vscode/package.json +++ b/components/clarity-vscode/package.json @@ -8,7 +8,7 @@ "homepage": "https://github.com/hirosystems/clarinet", "bugs": "https://github.com/hirosystems/clarinet/issues", "license": "GPL-3.0-only", - "version": "1.3.1", + "version": "1.3.2", "workspaces": [ "client", "server", @@ -89,11 +89,11 @@ "order": 0, "description": "If set to true, the completion won't wrap a function in parenthesis if an opening parenthesis is already there" }, - "clarity-lsp.completionIncludeParamsInSnippet": { + "clarity-lsp.completionIncludeNativePlaceholders": { "type": "boolean", "default": true, "order": 0, - "description": "If set to true, the completion of native functions will add the params snippet" + "description": "If set to true, the completion of native functions will include arguments placeholder. If set to false, it will only do so for functions interacting with variables, maps, fts and nfts." }, "clarity-lsp.hover": { "type": "boolean",