diff --git a/Cargo.toml b/Cargo.toml index 0edd271..4d680c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ thiserror = "2.0.17" ropey = "1.6.1" miniscript = "12" simplicityhl = { git = "https://github.com/BlockstreamResearch/SimplicityHL.git", rev = "e68e1c6" } +nom = "8.0.0" [lints.rust] unsafe_code = "deny" diff --git a/src/backend.rs b/src/backend.rs index 38c3d2d..78e8220 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -74,7 +74,7 @@ impl LanguageServer for Backend { )), completion_provider: Some(CompletionOptions { resolve_provider: Some(false), - trigger_characters: Some(vec![":".to_string()]), + trigger_characters: Some(vec![":".to_string(), "<".to_string()]), work_done_progress_options: WorkDoneProgressOptions::default(), all_commit_characters: None, completion_item: None, @@ -174,30 +174,12 @@ impl LanguageServer for Backend { "RopeSlice to str conversion failed".into(), ))?; - let trimmed_prefix = prefix.trim_end(); - - if let Some(last) = trimmed_prefix - .rsplit(|c: char| !c.is_alphanumeric() && c != ':') - .next() - { - if last.starts_with("jet:::") { - return Ok(Some(CompletionResponse::Array(vec![]))); - } else if last == "jet::" || last.starts_with("jet::") { - return Ok(Some(CompletionResponse::Array( - self.completion_provider.jets().to_vec(), - ))); - } - // Completion after a colon is needed only for jets. - } else if trimmed_prefix.ends_with(':') { - return Ok(Some(CompletionResponse::Array(vec![]))); - } - - let mut completions = - CompletionProvider::get_function_completions(&doc.functions.functions_and_docs()); - completions.extend_from_slice(self.completion_provider.builtins()); - completions.extend_from_slice(self.completion_provider.modules()); + let completions = self + .completion_provider + .process_completions(prefix, &doc.functions.functions_and_docs()) + .map(CompletionResponse::Array); - Ok(Some(CompletionResponse::Array(completions))) + Ok(completions) } async fn hover(&self, params: HoverParams) -> Result> { diff --git a/src/completion/mod.rs b/src/completion/mod.rs index 148b831..d0b3b59 100644 --- a/src/completion/mod.rs +++ b/src/completion/mod.rs @@ -2,12 +2,17 @@ use simplicityhl::parse::Function; pub mod builtin; pub mod jet; +pub mod tokens; +pub mod type_cast; pub mod types; use tower_lsp_server::lsp_types::{ CompletionItem, CompletionItemKind, Documentation, InsertTextFormat, MarkupContent, MarkupKind, }; +use tokens::Token; +use tokens::lex_tokens; + /// Build and provide [`CompletionItem`] for jets and builtin functions. #[derive(Debug)] pub struct CompletionProvider { @@ -19,6 +24,9 @@ pub struct CompletionProvider { /// Modules completions. modules: Vec, + + /// Default Type cast completions. + type_casts: Vec, } impl CompletionProvider { @@ -41,28 +49,28 @@ impl CompletionProvider { .iter() .map(|(module, detail)| module_to_completion((*module).to_string(), (*detail).to_string())) .collect(); + + let type_casts_completion = type_cast::TYPE_CASTS + .iter() + .map(|(&to, &from)| CompletionItem { + label: format!("{to} <- {from}"), + kind: Some(CompletionItemKind::FUNCTION), + detail: Some(format!("Cast into type `{to}`",)), + documentation: None, + insert_text: Some(format!("{from}>::into(${{1:{from}}})")), + insert_text_format: Some(InsertTextFormat::SNIPPET), + ..Default::default() + }) + .collect::>(); + Self { jets: jets_completion, builtin: builtin_completion, modules: modules_completion, + type_casts: type_casts_completion, } } - /// Return jets completions. - pub fn jets(&self) -> &[CompletionItem] { - &self.jets - } - - /// Return builtin functions completions. - pub fn builtins(&self) -> &[CompletionItem] { - &self.builtin - } - - /// Return builtin functions completions. - pub fn modules(&self) -> &[CompletionItem] { - &self.modules - } - /// Get generic functions completions. pub fn get_function_completions(functions: &[(&Function, &str)]) -> Vec { functions @@ -73,6 +81,80 @@ impl CompletionProvider { }) .collect() } + + /// Return completions based on line and functions provided. + pub fn process_completions( + &self, + prefix: &str, + functions: &[(&Function, &str)], + ) -> Option> { + let tokens = match lex_tokens(prefix) { + Ok((_, mut t)) => { + t.reverse(); + t + } + Err(_) => return None, + }; + + match tokens.as_slice() { + [Token::Jet, ..] => Some(self.jets.clone()), + + // Case for ": type = <", so we can return completion for specific type, or generic one + // if it is not on default type casts. + [ + Token::OpenAngle, + Token::EqualSign, + Token::Identifier(type_name), + Token::Colon, + .., + ] + | [ + Token::Identifier(_) | Token::OpenBracket, + Token::OpenAngle, + Token::EqualSign, + Token::Identifier(type_name), + Token::Colon, + .., + ] => { + let to = type_name.as_str(); + + if let Some(from) = type_cast::TYPE_CASTS.get(to) { + return Some(vec![CompletionItem { + label: format!("{to} <- {from}"), + kind: Some(CompletionItemKind::FUNCTION), + detail: Some(format!("Cast into type `{to}`",)), + documentation: None, + insert_text: Some(format!("{from}>::into(${{1:{from}}})")), + insert_text_format: Some(InsertTextFormat::SNIPPET), + ..Default::default() + }]); + } + Some(self.type_casts.clone()) + } + + // Case for ">::" -- this structure is only present for into keyword. + [Token::DoubleColon, Token::CloseAngle, ..] => Some(vec![CompletionItem { + label: "into".to_string(), + kind: Some(CompletionItemKind::FUNCTION), + detail: Some("Cast into type".to_string()), + documentation: None, + insert_text: Some("into(${1:type})".to_string()), + insert_text_format: Some(InsertTextFormat::SNIPPET), + ..Default::default() + }]), + + [Token::Colon | Token::OpenAngle, ..] => None, + + _ => { + let mut completions = CompletionProvider::get_function_completions(functions); + + completions.extend_from_slice(&self.builtin); + completions.extend_from_slice(&self.modules); + + Some(completions) + } + } + } } /// Convert [`simplicityhl::parse::Function`] to [`types::FunctionTemplate`]. diff --git a/src/completion/tokens.rs b/src/completion/tokens.rs new file mode 100644 index 0000000..ef77b2b --- /dev/null +++ b/src/completion/tokens.rs @@ -0,0 +1,65 @@ +use nom::{ + IResult, Parser, + branch::alt, + bytes::complete::{tag, take_while}, + character::complete::{multispace0, satisfy}, + combinator::{map, opt, recognize, value}, + multi::many0, + sequence::{pair, preceded}, +}; + +#[derive(Debug, PartialEq, Clone)] +pub enum Token { + Colon, + DoubleColon, + OpenAngle, + CloseAngle, + EqualSign, + OpenBracket, + ClosedBracket, + Identifier(String), + Jet, +} + +fn parse_symbol(input: &str) -> IResult<&str, Token> { + let mut parser = alt(( + value(Token::DoubleColon, tag("::")), + value(Token::Colon, tag(":")), + value(Token::OpenBracket, tag("(")), + value(Token::ClosedBracket, tag(")")), + value(Token::OpenAngle, tag("<")), + value(Token::CloseAngle, tag(">")), + value(Token::EqualSign, tag("=")), + )); + parser.parse(input) +} + +fn parse_jet(input: &str) -> IResult<&str, Token> { + let mut parser = value( + Token::Jet, + recognize(pair( + tag("jet::"), + opt(take_while(|c: char| c.is_alphanumeric() || c == '_')), + )), + ); + parser.parse(input) +} + +fn parse_identifier(input: &str) -> IResult<&str, Token> { + let mut parser = map( + recognize(pair( + satisfy(|c| c.is_alphabetic() || c == '_'), + take_while(|c: char| c.is_alphanumeric() || c == '_'), + )), + |s: &str| Token::Identifier(s.to_string()), + ); + parser.parse(input) +} + +pub fn lex_tokens(input: &str) -> IResult<&str, Vec> { + let mut parser = many0(preceded( + multispace0, + alt((parse_jet, parse_symbol, parse_identifier)), + )); + parser.parse(input) +} diff --git a/src/completion/type_cast.rs b/src/completion/type_cast.rs new file mode 100644 index 0000000..1ae98aa --- /dev/null +++ b/src/completion/type_cast.rs @@ -0,0 +1,25 @@ +use std::collections::HashMap; + +pub(crate) static TYPE_CASTS: std::sync::LazyLock> = + std::sync::LazyLock::new(|| { + HashMap::from([ + ("u1", "bool"), + ("u2", "(u1, u1)"), + ("u4", "(u2, u2)"), + ("u8", "(u4, u4)"), + ("u16", "(u8, u8)"), + ("u32", "(u16, u16)"), + ("u64", "(u32, u32)"), + ("u128", "(u64, u64)"), + ("u256", "(u128, u128)"), + ("bool", "u1"), + ("(u1, u1)", "u2"), + ("(u2, u2)", "u4"), + ("(u4, u4)", "u8"), + ("(u8, u8)", "u16"), + ("(u16, u16)", "u32"), + ("(u32, u32)", "u64"), + ("(u64, u64)", "u128"), + ("(u128, u128)", "u256"), + ]) + });