From 961df937e288d9e1e7a7ae0a8261e73c2d0aee5c Mon Sep 17 00:00:00 2001 From: Volodymyr Herashchenko Date: Wed, 12 Nov 2025 12:43:26 +0200 Subject: [PATCH 1/6] move completion processing into `CompletionProvider` as we plan to expand completion options, it's logical to move parsing away from backend.rs. --- src/backend.rs | 28 +++++----------------------- src/completion/mod.rs | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/backend.rs b/src/backend.rs index 38c3d2d..1288156 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -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..d1106e5 100644 --- a/src/completion/mod.rs +++ b/src/completion/mod.rs @@ -73,6 +73,30 @@ impl CompletionProvider { }) .collect() } + + pub fn process_completions( + &self, + prefix: &str, + functions: &[(&Function, &str)], + ) -> Option> { + if let Some(last) = prefix + .rsplit(|c: char| !c.is_alphanumeric() && c != ':') + .next() + { + if last == "jet::" || last.starts_with("jet::") { + return Some(self.jets().to_vec()); + } + } + if prefix.ends_with(':') { + return None; + } + + let mut completions = CompletionProvider::get_function_completions(functions); + completions.extend_from_slice(self.builtins()); + completions.extend_from_slice(self.modules()); + + Some(completions) + } } /// Convert [`simplicityhl::parse::Function`] to [`types::FunctionTemplate`]. From 2966210f4fc64e26031decf12a3573ddbabbd476 Mon Sep 17 00:00:00 2001 From: Volodymyr Herashchenko Date: Wed, 12 Nov 2025 16:36:04 +0200 Subject: [PATCH 2/6] add type cast completions for default integers but not removing `into` snippet yet, because it can be potentially moved into `snippets` completions, and it not interfere with new `into` completions --- src/backend.rs | 2 +- src/completion/mod.rs | 56 +++++++++++++++++++++++-------------- src/completion/type_cast.rs | 25 +++++++++++++++++ 3 files changed, 61 insertions(+), 22 deletions(-) create mode 100644 src/completion/type_cast.rs diff --git a/src/backend.rs b/src/backend.rs index 1288156..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, diff --git a/src/completion/mod.rs b/src/completion/mod.rs index d1106e5..017eb58 100644 --- a/src/completion/mod.rs +++ b/src/completion/mod.rs @@ -2,6 +2,7 @@ use simplicityhl::parse::Function; pub mod builtin; pub mod jet; +pub mod type_cast; pub mod types; use tower_lsp_server::lsp_types::{ @@ -19,6 +20,9 @@ pub struct CompletionProvider { /// Modules completions. modules: Vec, + + /// Default Type cast completions. + type_casts: Vec, } impl CompletionProvider { @@ -41,28 +45,28 @@ impl CompletionProvider { .iter() .map(|(module, detail)| module_to_completion((*module).to_string(), (*detail).to_string())) .collect(); + + let type_casts_completion = type_cast::get_integer_type_casts() + .iter() + .map(|(&to, &from)| CompletionItem { + label: format!("<{from}>::into({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 @@ -74,26 +78,36 @@ impl CompletionProvider { .collect() } + /// Return completions based on line and functions provided. pub fn process_completions( &self, prefix: &str, functions: &[(&Function, &str)], ) -> Option> { if let Some(last) = prefix - .rsplit(|c: char| !c.is_alphanumeric() && c != ':') + .rsplit(|c: char| !c.is_alphanumeric() && c != ':' && c != '<') .next() { - if last == "jet::" || last.starts_with("jet::") { - return Some(self.jets().to_vec()); + if last.starts_with("jet::") { + return Some(self.jets.clone()); + } + if last.starts_with('<') { + return Some(self.type_casts.clone()); } } + if prefix.ends_with(':') { return None; } - let mut completions = CompletionProvider::get_function_completions(functions); - completions.extend_from_slice(self.builtins()); - completions.extend_from_slice(self.modules()); + // return only function completions in case of '<' symbol in `for_while`, `array_fold` and + // `fold` + if prefix.ends_with('<') { + return Some(completions); + } + + completions.extend_from_slice(&self.builtin); + completions.extend_from_slice(&self.modules); Some(completions) } diff --git a/src/completion/type_cast.rs b/src/completion/type_cast.rs new file mode 100644 index 0000000..95cdf56 --- /dev/null +++ b/src/completion/type_cast.rs @@ -0,0 +1,25 @@ +use std::collections::HashMap; + +pub fn get_integer_type_casts() -> HashMap<&'static str, &'static str> { + 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"), + ]) +} + From 698c2b44a85b9143a2e206b2f51be074a573f8d0 Mon Sep 17 00:00:00 2001 From: Volodymyr Herashchenko Date: Wed, 12 Nov 2025 16:46:06 +0200 Subject: [PATCH 3/6] fmt: remove trailing whitespace --- src/completion/type_cast.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/completion/type_cast.rs b/src/completion/type_cast.rs index 95cdf56..5d3cfa2 100644 --- a/src/completion/type_cast.rs +++ b/src/completion/type_cast.rs @@ -22,4 +22,3 @@ pub fn get_integer_type_casts() -> HashMap<&'static str, &'static str> { ("(u128, u128)", "u256"), ]) } - From 523d19361c96473ea47b8f2b06e965e2dd767f34 Mon Sep 17 00:00:00 2001 From: Volodymyr Herashchenko Date: Thu, 13 Nov 2025 15:37:02 +0200 Subject: [PATCH 4/6] change typecast function to lazyload --- src/completion/mod.rs | 2 ++ src/completion/type_cast.rs | 45 +++++++++++++++++++------------------ 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/completion/mod.rs b/src/completion/mod.rs index 017eb58..47b2ea1 100644 --- a/src/completion/mod.rs +++ b/src/completion/mod.rs @@ -47,9 +47,11 @@ impl CompletionProvider { .collect(); let type_casts_completion = type_cast::get_integer_type_casts() + let type_casts_completion = type_cast::TYPE_CASTS .iter() .map(|(&to, &from)| CompletionItem { label: format!("<{from}>::into({from})"), + label: format!("{to} <- {from}"), kind: Some(CompletionItemKind::FUNCTION), detail: Some(format!("Cast into type `{to}`",)), documentation: None, diff --git a/src/completion/type_cast.rs b/src/completion/type_cast.rs index 5d3cfa2..1ae98aa 100644 --- a/src/completion/type_cast.rs +++ b/src/completion/type_cast.rs @@ -1,24 +1,25 @@ use std::collections::HashMap; -pub fn get_integer_type_casts() -> HashMap<&'static str, &'static str> { - 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"), - ]) -} +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"), + ]) + }); From 9eebde09f7685df545a15266714aa37e349f7aae Mon Sep 17 00:00:00 2001 From: Volodymyr Herashchenko Date: Thu, 13 Nov 2025 15:39:03 +0200 Subject: [PATCH 5/6] add logos crate for completions As we add more specific cases, we should move away from simple string processing toward more flexible methods. The `logos` crate claims to be very fast and is more pleasant to work with than standard string manipulation or regex. --- Cargo.toml | 1 + src/completion/mod.rs | 79 ++++++++++++++++++++++++++++------------ src/completion/tokens.rs | 28 ++++++++++++++ 3 files changed, 84 insertions(+), 24 deletions(-) create mode 100644 src/completion/tokens.rs diff --git a/Cargo.toml b/Cargo.toml index 0edd271..c5de08c 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" } +logos = "0.15.1" [lints.rust] unsafe_code = "deny" diff --git a/src/completion/mod.rs b/src/completion/mod.rs index 47b2ea1..20204fe 100644 --- a/src/completion/mod.rs +++ b/src/completion/mod.rs @@ -1,7 +1,9 @@ +use logos::Logos; use simplicityhl::parse::Function; pub mod builtin; pub mod jet; +pub mod tokens; pub mod type_cast; pub mod types; @@ -9,6 +11,8 @@ use tower_lsp_server::lsp_types::{ CompletionItem, CompletionItemKind, Documentation, InsertTextFormat, MarkupContent, MarkupKind, }; +use tokens::Token; + /// Build and provide [`CompletionItem`] for jets and builtin functions. #[derive(Debug)] pub struct CompletionProvider { @@ -46,11 +50,9 @@ impl CompletionProvider { .map(|(module, detail)| module_to_completion((*module).to_string(), (*detail).to_string())) .collect(); - let type_casts_completion = type_cast::get_integer_type_casts() let type_casts_completion = type_cast::TYPE_CASTS .iter() .map(|(&to, &from)| CompletionItem { - label: format!("<{from}>::into({from})"), label: format!("{to} <- {from}"), kind: Some(CompletionItemKind::FUNCTION), detail: Some(format!("Cast into type `{to}`",)), @@ -86,32 +88,61 @@ impl CompletionProvider { prefix: &str, functions: &[(&Function, &str)], ) -> Option> { - if let Some(last) = prefix - .rsplit(|c: char| !c.is_alphanumeric() && c != ':' && c != '<') - .next() - { - if last.starts_with("jet::") { - return Some(self.jets.clone()); - } - if last.starts_with('<') { - return Some(self.type_casts.clone()); + let mut tokens: Vec = Token::lexer(prefix).filter_map(Result::ok).collect(); + tokens.reverse(); + + match tokens.as_slice() { + [Token::Jet, ..] => Some(self.jets.clone()), + + [ + 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()) } - } - if prefix.ends_with(':') { - return None; - } - let mut completions = CompletionProvider::get_function_completions(functions); - // return only function completions in case of '<' symbol in `for_while`, `array_fold` and - // `fold` - if prefix.ends_with('<') { - return Some(completions); - } + [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, - completions.extend_from_slice(&self.builtin); - completions.extend_from_slice(&self.modules); + _ => { + let mut completions = CompletionProvider::get_function_completions(functions); + // return only function completions in case of '<' symbol in `for_while`, `array_fold` and + // `fold` + if prefix.ends_with('<') { + return Some(completions); + } - Some(completions) + completions.extend_from_slice(&self.builtin); + completions.extend_from_slice(&self.modules); + + Some(completions) + } + } } } diff --git a/src/completion/tokens.rs b/src/completion/tokens.rs new file mode 100644 index 0000000..e11af0a --- /dev/null +++ b/src/completion/tokens.rs @@ -0,0 +1,28 @@ +use logos::Logos; + +#[derive(Logos, Debug, PartialEq, Clone)] +#[logos(skip r"[ \t\r\n\f]+")] +pub enum Token { + #[token(":")] + Colon, + + #[token("::")] + DoubleColon, + + #[token("<")] + OpenAngle, + + #[token(">")] + CloseAngle, + + #[token("=")] + EqualSign, + + #[regex(r"\(\s*[a-zA-Z_][a-zA-Z0-9_]*\s*,\s*[a-zA-Z_][a-zA-Z0-9_]*\s*\)", |lex| lex.slice().to_string())] + #[regex(r"[a-zA-Z_][a-zA-Z0-9_]*", |lex| lex.slice().to_string())] + Identifier(String), + + #[regex(r"jet::[a-zA-Z0-9_]?", priority = 2)] + #[token("jet::", priority = 1)] + Jet, +} From f6a198454f717dd54c562706316e7e2ac27dde93 Mon Sep 17 00:00:00 2001 From: Volodymyr Herashchenko Date: Mon, 17 Nov 2025 17:41:12 +0200 Subject: [PATCH 6/6] replace `logos` with `nom` nom is more lightweight and more suitable for this task --- Cargo.toml | 2 +- src/completion/mod.rs | 27 ++++++++++----- src/completion/tokens.rs | 73 ++++++++++++++++++++++++++++++---------- 3 files changed, 75 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c5de08c..4d680c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ thiserror = "2.0.17" ropey = "1.6.1" miniscript = "12" simplicityhl = { git = "https://github.com/BlockstreamResearch/SimplicityHL.git", rev = "e68e1c6" } -logos = "0.15.1" +nom = "8.0.0" [lints.rust] unsafe_code = "deny" diff --git a/src/completion/mod.rs b/src/completion/mod.rs index 20204fe..d0b3b59 100644 --- a/src/completion/mod.rs +++ b/src/completion/mod.rs @@ -1,4 +1,3 @@ -use logos::Logos; use simplicityhl::parse::Function; pub mod builtin; @@ -12,6 +11,7 @@ use tower_lsp_server::lsp_types::{ }; use tokens::Token; +use tokens::lex_tokens; /// Build and provide [`CompletionItem`] for jets and builtin functions. #[derive(Debug)] @@ -88,18 +88,33 @@ impl CompletionProvider { prefix: &str, functions: &[(&Function, &str)], ) -> Option> { - let mut tokens: Vec = Token::lexer(prefix).filter_map(Result::ok).collect(); - tokens.reverse(); + 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(); @@ -117,6 +132,7 @@ impl CompletionProvider { 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), @@ -131,11 +147,6 @@ impl CompletionProvider { _ => { let mut completions = CompletionProvider::get_function_completions(functions); - // return only function completions in case of '<' symbol in `for_while`, `array_fold` and - // `fold` - if prefix.ends_with('<') { - return Some(completions); - } completions.extend_from_slice(&self.builtin); completions.extend_from_slice(&self.modules); diff --git a/src/completion/tokens.rs b/src/completion/tokens.rs index e11af0a..ef77b2b 100644 --- a/src/completion/tokens.rs +++ b/src/completion/tokens.rs @@ -1,28 +1,65 @@ -use logos::Logos; +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(Logos, Debug, PartialEq, Clone)] -#[logos(skip r"[ \t\r\n\f]+")] +#[derive(Debug, PartialEq, Clone)] pub enum Token { - #[token(":")] Colon, - - #[token("::")] DoubleColon, - - #[token("<")] OpenAngle, - - #[token(">")] CloseAngle, - - #[token("=")] EqualSign, - - #[regex(r"\(\s*[a-zA-Z_][a-zA-Z0-9_]*\s*,\s*[a-zA-Z_][a-zA-Z0-9_]*\s*\)", |lex| lex.slice().to_string())] - #[regex(r"[a-zA-Z_][a-zA-Z0-9_]*", |lex| lex.slice().to_string())] + OpenBracket, + ClosedBracket, Identifier(String), - - #[regex(r"jet::[a-zA-Z0-9_]?", priority = 2)] - #[token("jet::", priority = 1)] 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) +}