diff --git a/Cargo.toml b/Cargo.toml index 8c73e84..96c8c7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,27 @@ [package] name = "simplicityhl-lsp" -version = "0.1.2" +version = "0.1.3" edition = "2024" +rust-version = "1.87" [dependencies] tokio = { version = "1.47.1", features = ["full"] } serde_json = "1.0.143" tower-lsp-server = "0.22.1" -simplicityhl = { git = "https://github.com/BlockstreamResearch/SimplicityHL.git", rev = "e68e1c6" } -dashmap = "6.1.0" -ropey = "1.6.1" + log = "0.4.28" env_logger = "0.11.8" + +ropey = "1.6.1" +miniscript = "12" +simplicityhl = { git = "https://github.com/BlockstreamResearch/SimplicityHL.git", rev = "e68e1c6" } + +[lints.rust] +unsafe_code = "deny" +unused_variables = "warn" +dead_code = "warn" +unreachable_code = "warn" +unused_mut = "warn" + +[lints.clippy] +pedantic = "warn" diff --git a/src/backend.rs b/src/backend.rs index e4ce77f..30ac28f 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -1,32 +1,40 @@ -use dashmap::DashMap; use ropey::Rope; use serde_json::Value; +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::Arc; +use tokio::sync::RwLock; -use tower_lsp_server::jsonrpc::{Error, Result}; +use tower_lsp_server::jsonrpc::Result; use tower_lsp_server::lsp_types::{ CompletionOptions, CompletionParams, CompletionResponse, Diagnostic, DidChangeConfigurationParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, - DidSaveTextDocumentParams, ExecuteCommandParams, InitializeParams, InitializeResult, - InitializedParams, MessageType, OneOf, Position, Range, SaveOptions, SemanticTokensParams, - SemanticTokensResult, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, - TextDocumentSyncOptions, TextDocumentSyncSaveOptions, Uri, WorkDoneProgressOptions, - WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities, + DidSaveTextDocumentParams, ExecuteCommandParams, Hover, HoverParams, HoverProviderCapability, + InitializeParams, InitializeResult, InitializedParams, MarkupContent, MarkupKind, MessageType, + OneOf, Range, SaveOptions, SemanticTokensParams, SemanticTokensResult, ServerCapabilities, + TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, + TextDocumentSyncSaveOptions, Uri, WorkDoneProgressOptions, WorkspaceFoldersServerCapabilities, + WorkspaceServerCapabilities, }; use tower_lsp_server::{Client, LanguageServer}; use simplicityhl::{ ast, - error::{RichError, Span, WithFile}, + error::{RichError, WithFile}, parse, parse::ParseFromStr, }; -use crate::completion::CompletionProvider; +use miniscript::iter::TreeLike; + +use crate::completion::{self, CompletionProvider}; +use crate::utils::{positions_to_span, span_contains, span_to_positions}; #[derive(Debug)] struct Document { functions: Vec, + functions_docs: HashMap, text: Rope, } @@ -34,7 +42,7 @@ struct Document { pub struct Backend { client: Client, - document_map: DashMap, + document_map: Arc>>, completion_provider: CompletionProvider, } @@ -74,6 +82,7 @@ impl LanguageServer for Backend { }), file_operations: None, }), + hover_provider: Some(HoverProviderCapability::Simple(true)), ..ServerCapabilities::default() }, }) @@ -136,28 +145,62 @@ impl LanguageServer for Backend { async fn completion(&self, params: CompletionParams) -> Result> { let uri = ¶ms.text_document_position.text_document.uri; let pos = params.text_document_position.position; + let documents = self.document_map.read().await; + + let Some(doc) = documents.get(uri) else { + return Ok(Some(CompletionResponse::Array(vec![]))); + }; - let Some(document) = self.document_map.get(uri) else { + let Some(line) = doc.text.lines().nth(pos.line as usize) else { return Ok(Some(CompletionResponse::Array(vec![]))); }; - let Some(line) = document.text.lines().nth(pos.line as usize) else { + let Some(slice) = line.get_slice(..pos.character as usize) else { return Ok(Some(CompletionResponse::Array(vec![]))); }; - let Some(prefix) = line.slice(..pos.character as usize).as_str() else { + let Some(prefix) = slice.as_str() else { return Ok(Some(CompletionResponse::Array(vec![]))); }; - if prefix.ends_with("jet::") { - return Ok(Some(CompletionResponse::Array( - self.completion_provider.jets().to_vec(), - ))); + 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 colon needed only for jets + } else if trimmed_prefix.ends_with(':') { + return Ok(Some(CompletionResponse::Array(vec![]))); } - Ok(Some(CompletionResponse::Array( - CompletionProvider::get_function_completions(document.functions.as_slice()), - ))) + let mut completions = CompletionProvider::get_function_completions( + &doc.functions + .iter() + .map(|func| { + let function_doc = doc + .functions_docs + .get(&func.name().to_string()) + .map_or(String::new(), String::clone); + (func.to_owned(), function_doc) + }) + .collect::>(), + ); + completions.extend_from_slice(self.completion_provider.builtins()); + completions.extend_from_slice(self.completion_provider.modules()); + + Ok(Some(CompletionResponse::Array(completions))) + } + + async fn hover(&self, params: HoverParams) -> Result> { + Ok(self.provide_hover(¶ms).await) } } @@ -165,53 +208,24 @@ impl Backend { pub fn new(client: Client) -> Self { Self { client, - document_map: DashMap::new(), + document_map: Arc::new(RwLock::new(HashMap::new())), completion_provider: CompletionProvider::new(), } } - fn parse_program(&self, text: &str, uri: &Uri) -> Option { - let parse_program = match parse::Program::parse_from_str(text) { - Ok(p) => p, - Err(e) => return Some(e), - }; - - parse_program - .items() - .iter() - .filter_map(|item| { - if let parse::Item::Function(func) = item { - Some(func) - } else { - None - } - }) - .for_each(|func| { - if let Some(mut doc) = self.document_map.get_mut(uri) { - doc.functions.push(func.to_owned()); - } - }); - - ast::Program::analyze(&parse_program).with_file(text).err() - } - + /// Function which executed on change of file (`did_save`, `did_open` or `did_change` methods) async fn on_change(&self, params: TextDocumentItem<'_>) { - let rope = ropey::Rope::from_str(params.text); - self.document_map.insert( - params.uri.clone(), - Document { - functions: vec![], - text: rope.clone(), - }, - ); + let (err, document) = parse_program(params.text); - let err = self.parse_program(params.text, ¶ms.uri); + let mut documents = self.document_map.write().await; + if let Some(doc) = document { + documents.insert(params.uri.clone(), doc); + } else if let Some(doc) = documents.get_mut(¶ms.uri) { + doc.text = Rope::from_str(params.text); + } match err { None => { - self.client - .log_message(MessageType::INFO, "errors not found!".to_string()) - .await; self.client .publish_diagnostics(params.uri.clone(), vec![], params.version) .await; @@ -220,7 +234,12 @@ impl Backend { let (start, end) = match span_to_positions(err.span()) { Ok(result) => result, Err(err) => { - dbg!("catch error: {}", err); + self.client + .log_message( + MessageType::ERROR, + format!("Catch error while parsing span: {err}"), + ) + .await; return; } }; @@ -238,31 +257,191 @@ impl Backend { } } } + + /// Provide hover for [`Backend::hover`] function. + async fn provide_hover(&self, params: &HoverParams) -> Option { + let documents = self.document_map.read().await; + + let document = documents.get(¶ms.text_document_position_params.text_document.uri)?; + + let token_position = params.text_document_position_params.position; + let token_span = positions_to_span((token_position, token_position)).ok()?; + + let call = find_related_call(&document.functions, token_span)?; + let (start, end) = span_to_positions(call.span()).ok()?; + + let description = match call.name() { + parse::CallName::Jet(jet) => { + let element = + simplicityhl::simplicity::jet::Elements::from_str(format!("{jet}").as_str()) + .ok()?; + + let template = completion::jet::jet_to_template(element); + format!( + "```simplicityhl\nfn jet::{}({}) -> {}\n```\n{}", + template.display_name, + template.args.join(", "), + template.return_type, + template.description + ) + } + parse::CallName::Custom(func) => { + let function = document.functions.iter().find(|f| f.name() == func)?; + let function_doc = document.functions_docs.get(&func.to_string())?; + + let template = completion::function_to_template(function, function_doc); + format!( + "```simplicityhl\nfn {}({}) -> {}\n```\n{}", + template.display_name, + template.args.join(", "), + template.return_type, + template.description + ) + } + other => { + let template = completion::builtin::match_callname(other)?; + format!( + "```simplicityhl\nfn {}({}) -> {}\n```\n{}", + template.display_name, + template.args.join(", "), + template.return_type, + template.description + ) + } + }; + + Some(Hover { + contents: tower_lsp_server::lsp_types::HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: description, + }), + range: Some(Range { start, end }), + }) + } +} + +/// Create [`Document`] using parsed program and code. +fn create_document(program: &simplicityhl::parse::Program, text: &str) -> Document { + let mut document = Document { + functions: vec![], + functions_docs: HashMap::new(), + text: Rope::from_str(text), + }; + + program + .items() + .iter() + .filter_map(|item| { + if let parse::Item::Function(func) = item { + Some(func) + } else { + None + } + }) + .for_each(|func| { + let start_line = u32::try_from(func.as_ref().start.line.get()).unwrap_or_default() - 1; + + document.functions.push(func.to_owned()); + document.functions_docs.insert( + func.name().to_string(), + get_comments_from_lines(start_line, &document.text), + ); + }); + + document +} + +/// Parse program using [`simplicityhl`] compiler and return [`RichError`], +/// which used in Diagnostic. Also create [`Document`] from parsed program. +fn parse_program(text: &str) -> (Option, Option) { + let program = match parse::Program::parse_from_str(text) { + Ok(p) => p, + Err(e) => return (Some(e), None), + }; + + ( + ast::Program::analyze(&program).with_file(text).err(), + Some(create_document(&program, text)), + ) +} + +/// Get document comments, using lines above given line index. Only used to +/// get documentation for custom functions. +fn get_comments_from_lines(line: u32, rope: &Rope) -> String { + let mut lines = Vec::new(); + + if line == 0 { + return String::new(); + } + + for i in (0..line).rev() { + let Some(rope_slice) = rope.get_line(i as usize) else { + break; + }; + let text = rope_slice.to_string(); + + if text.starts_with("///") { + let doc = text + .strip_prefix("///") + .unwrap_or("") + .trim_end() + .to_string(); + lines.push(doc); + } else { + break; + } + } + + lines.reverse(); + + let mut result = String::new(); + let mut prev_line_was_text = false; + + for line in lines { + let trimmed = line.trim(); + + let is_md_block = trimmed.is_empty() + || trimmed.starts_with('#') + || trimmed.starts_with('-') + || trimmed.starts_with('*') + || trimmed.starts_with('>') + || trimmed.starts_with("```") + || trimmed.starts_with(" "); + + if result.is_empty() { + result.push_str(trimmed); + } else if prev_line_was_text && !is_md_block { + result.push(' '); + result.push_str(trimmed); + } else { + result.push('\n'); + result.push_str(trimmed); + } + + prev_line_was_text = !trimmed.is_empty() && !is_md_block; + } + + result } -/// Convert `simplicityhl::error::Span` to `tower_lsp_server::lsp_types::Positions` -/// -/// Converting is required because `simplicityhl::error::Span` using their own versions of `Position`, -/// which contains non-zero column and line, so they are always starts with one. -/// `Position` required for diagnostic starts with zero -fn span_to_positions(span: &Span) -> Result<(Position, Position)> { - let start_line = u32::try_from(span.start.line.get()) - .map_err(|e| Error::invalid_params(format!("line overflow: {e}")))?; - let start_col = u32::try_from(span.start.col.get()) - .map_err(|e| Error::invalid_params(format!("col overflow: {e}")))?; - let end_line = u32::try_from(span.end.line.get()) - .map_err(|e| Error::invalid_params(format!("line overflow: {e}")))?; - let end_col = u32::try_from(span.end.col.get()) - .map_err(|e| Error::invalid_params(format!("col overflow: {e}")))?; - - Ok(( - Position { - line: start_line - 1, - character: start_col - 1, - }, - Position { - line: end_line - 1, - character: end_col - 1, - }, - )) +/// Find [`simplicityhl::parse::Call`] which contains given [`simplicityhl::error::Span`], which also have minimal Span. +fn find_related_call( + functions: &[parse::Function], + token_span: simplicityhl::error::Span, +) -> Option<&simplicityhl::parse::Call> { + let func = functions + .iter() + .find(|func| span_contains(func.span(), &token_span))?; + + parse::ExprTree::Expression(func.body()) + .pre_order_iter() + .filter_map(|expr| { + if let parse::ExprTree::Call(call) = expr { + Some(call) + } else { + None + } + }) + .filter(|c| span_contains(c.span(), &token_span)) + .last() } diff --git a/src/completion.rs b/src/completion.rs deleted file mode 100644 index 4e59b11..0000000 --- a/src/completion.rs +++ /dev/null @@ -1,96 +0,0 @@ -use simplicityhl::jet; -use simplicityhl::parse::Function; -use simplicityhl::simplicity::jet::Elements; - -use crate::jet::documentation; - -use tower_lsp_server::lsp_types::{ - CompletionItem, CompletionItemKind, Documentation, InsertTextFormat, -}; - -#[derive(Debug)] -pub struct CompletionProvider { - jets_completion: Vec, -} - -impl CompletionProvider { - pub fn new() -> Self { - let jets_completion = Elements::ALL - .iter() - .copied() - .map(jet_to_completion_item) - .collect(); - - Self { jets_completion } - } - - pub fn jets(&self) -> &[CompletionItem] { - &self.jets_completion - } - - pub fn get_function_completions(functions: &[Function]) -> Vec { - functions.iter().map(function_to_completion_item).collect() - } -} - -fn jet_to_completion_item(jet: Elements) -> CompletionItem { - let name = jet.to_string(); - CompletionItem { - label: name.clone(), - kind: Some(CompletionItemKind::FUNCTION), - detail: Some(format!( - "fn({}) -> {}", - jet::source_type(jet) - .iter() - .map(|item| { format!("{item}") }) - .collect::>() - .join(", "), - jet::target_type(jet) - )), - documentation: Some(Documentation::String(documentation(jet).to_string())), - insert_text: Some(format!( - "{}({})", - name, - jet::source_type(jet) - .iter() - .enumerate() - .map(|(index, item)| { format!("${{{}:{}}}", index + 1, item) }) - .collect::>() - .join(", ") - )), - insert_text_format: Some(InsertTextFormat::SNIPPET), - ..Default::default() - } -} - -fn function_to_completion_item(func: &Function) -> CompletionItem { - let name = func.name().to_string(); - CompletionItem { - label: name.clone(), - kind: Some(CompletionItemKind::FUNCTION), - detail: Some(format!( - "fn({}) -> {}", - func.params() - .iter() - .map(|item| { format!("{}", item.ty()) }) - .collect::>() - .join(", "), - match func.ret() { - Some(ret) => format!("{ret}"), - None => "()".to_string(), - } - )), - insert_text: Some(format!( - "{}({})", - name, - func.params() - .iter() - .enumerate() - .map(|(index, item)| { format!("${{{}:{}}}", index + 1, item) }) - .collect::>() - .join(", ") - )), - insert_text_format: Some(InsertTextFormat::SNIPPET), - ..Default::default() - } -} diff --git a/src/completion/builtin.rs b/src/completion/builtin.rs new file mode 100644 index 0000000..f8c3994 --- /dev/null +++ b/src/completion/builtin.rs @@ -0,0 +1,217 @@ +use std::num::NonZero; + +use simplicityhl::{ + num::NonZeroPow2Usize, + parse::CallName, + str::{AliasName, FunctionName}, + types::AliasedType, +}; + +use crate::completion::types::FunctionTemplate; + +/// Get completion of builtin functions. They are all defined in [`simplicityhl::parse::CallName`] +pub fn get_builtin_functions() -> Vec { + let ty = AliasedType::from(AliasName::from_str_unchecked("T")); + let Some(some) = NonZero::new(1) else { + return vec![]; + }; + + let functions = vec![ + CallName::UnwrapLeft(ty.clone()), + CallName::UnwrapRight(ty.clone()), + CallName::Unwrap, + CallName::IsNone(ty.clone()), + CallName::Assert, + CallName::Debug, + CallName::Panic, + CallName::Fold( + FunctionName::from_str_unchecked("name"), + NonZeroPow2Usize::TWO, + ), + CallName::ArrayFold(FunctionName::from_str_unchecked("name"), some), + CallName::ForWhile(FunctionName::from_str_unchecked("name")), + ]; + + functions.iter().filter_map(match_callname).collect() +} + +/// Match [`simplicityhl::parse::CallName`] and return [`FunctionTemplate`] +pub fn match_callname(call: &CallName) -> Option { + let doc = builtin_documentation(call); + match call { + CallName::UnwrapLeft(aliased_type) => { + let ty = aliased_type.to_string(); + Some(FunctionTemplate::new( + "unwrap_left", + "unwrap_left", + vec![format!("{ty}")], + vec![format!("Either<{ty}, U>")], + ty, + doc, + )) + } + CallName::UnwrapRight(aliased_type) => { + let ty = aliased_type.to_string(); + Some(FunctionTemplate::new( + "unwrap_right", + "unwrap_right", + vec![format!("{ty}")], + vec![format!("Either")], + ty, + doc, + )) + } + CallName::Unwrap => Some(FunctionTemplate::simple( + "unwrap", + vec!["Option".to_string()], + "T", + doc, + )), + CallName::IsNone(aliased_type) => { + let ty = aliased_type.to_string(); + Some(FunctionTemplate::new( + "is_none".to_string(), + "is_none", + vec![format!("{ty}")], + vec![format!("Option<{ty}>")], + "bool", + doc, + )) + } + CallName::Assert => Some(FunctionTemplate::simple( + "assert!", + vec!["condition: bool".to_string()], + "()", + doc, + )), + CallName::Panic => Some(FunctionTemplate::simple("panic!", vec![], "()", doc)), + CallName::Debug => Some(FunctionTemplate::simple( + "dbg!", + vec!["T".to_string()], + "T", + doc, + )), + CallName::Fold(_, _) => Some(FunctionTemplate::new( + "fold", + "fold", + vec!["f".to_string(), "N".to_string()], + vec![ + "list: List".to_string(), + "initial_accumulator: A".to_string(), + ], + "A", + doc, + )), + CallName::ArrayFold(_, _) => Some(FunctionTemplate::new( + "array_fold", + "array_fold", + vec!["f".to_string(), "N".to_string()], + vec![ + "array: [E; N]".to_string(), + "initial_accumulator: A".to_string(), + ], + "A", + doc, + )), + CallName::ForWhile(_) => Some(FunctionTemplate::new( + "for_while", + "for_while", + vec!["f".to_string()], + vec!["accumulator: A".to_string(), "context: C".to_string()], + "Either", + doc, + )), + // TODO: implement TypeCast definition + CallName::Jet(_) | CallName::TypeCast(_) | CallName::Custom(_) => None, + } +} + +fn builtin_documentation(call: &CallName) -> String { + String::from(match call { + CallName::UnwrapLeft(_) => + "Extracts the left variant of an `Either` value.\n +Returns the left-side value if it exists, otherwise panics.\n +```simplicityhl +let x: Either = Left(42); +let y: u8 = unwrap_left::(x); // 42 +```", + CallName::UnwrapRight(_) => + "Extracts the right variant of an `Either` value.\n +Returns the right-side value if it exists, otherwise panics.\n +```simplicityhl +let x: Either = Right(128); +let y: u8 = unwrap_right::(x); // 128 +```", + CallName::Unwrap => + "Unwraps an `Option` value, panicking if it is `None`.\n +```simplicityhl +let x: Option = Some(5); +let y: u8 = unwrap(x); // 5 +```", + CallName::IsNone(_) => + "Checks if an `Option` is `None`.\n +Returns `true` if the value is `None`, otherwise `false`. +", + CallName::Assert => "Panics when `condition` is false.", + CallName::Panic => "Unconditionally terminates program execution.", + CallName::Debug => + "Prints a value if debugging symbols is enabled and returns it unchanged. \n +```simplicityhl +let x: u32 = dbg!(42); // prints 42, returns 42 +```", + CallName::Fold(_, _) => + "Fold a list of bounded length by repeatedly applying a function.\n +- Signature: `fold::(list: List, initial_accumulator: A) -> A` +- Fold step: `fn f(element: E, acc: A) -> A` +- Note: `N` is a power of two; lists hold fewer than `N` elements.\n +Example: sum a list of 32-bit integers.\n +```simplicityhl +fn sum(elt: u32, acc: u32) -> u32 { + let (_, acc): (bool, u32) = jet::add_32(elt, acc); + acc +} + +fn main() { + let xs: List = list![1, 2, 3]; + let s: u32 = fold::(xs, 0); + assert!(jet::eq_32(s, 6)); +} +```", + CallName::ArrayFold(_, _) => + "Fold a fixed-size array by repeatedly applying a function.\n +- Signature: `array_fold::(array: [E; N], initial_accumulator: A) -> A` +- Fold step: `fn f(element: E, acc: A) -> A`\n +Example: sum an array of 7 elements.\n +```simplicityhl +fn sum(elt: u32, acc: u32) -> u32 { + let (_, acc): (bool, u32) = jet::add_32(elt, acc); + acc +} + +fn main() { + let arr: [u32; 7] = [1, 2, 3, 4, 5, 6, 7]; + let sum: u32 = array_fold::(arr, 0); + assert!(jet::eq_32(sum, 28)); +} +```", + CallName::ForWhile(_) => + "Run a function `f` repeatedly with a bounded counter. The loop stops early when the function returns a successful value.\n +- Signature: `for_while::(initial_accumulator: A, readonly_context: C) -> Either` +- Loop body: `fn f(acc: A, ctx: C, counter: uN) -> Either` where `N ∈ {1, 2, 4, 8, 16}`\n +Example: stop when `counter == 10`.\n +```simplicityhl +fn stop_at_10(acc: (), _: (), i: u8) -> Either { + match jet::eq_8(i, 10) { + true => Left(i), // success → exit loop + false => Right(acc), // continue with same accumulator + } +} + +fn main() { + let out: Either = for_while::((), ()); + assert!(jet::eq_8(10, unwrap_left::<()>(out))); +} +```", + CallName::Jet(_) | CallName::TypeCast(_) | CallName::Custom(_) => "", + }) +} diff --git a/src/jet.rs b/src/completion/jet.rs similarity index 98% rename from src/jet.rs rename to src/completion/jet.rs index 0ee346c..169149d 100644 --- a/src/jet.rs +++ b/src/completion/jet.rs @@ -1,9 +1,28 @@ -#![allow(warnings)] - -// copied from https://github.com/BlockstreamResearch/SimplicityHL/blob/master/codegen/src/jet.rs +use crate::completion::types; +use simplicityhl::jet; use simplicityhl::simplicity::jet::Elements; +/// Convert all jets to [`types::FunctionTemplate`]. +pub fn get_jets_completions() -> Vec { + Elements::ALL.iter().copied().map(jet_to_template).collect() +} + +/// Convert [`Elements`] to [`types::FunctionTemplate`] +pub fn jet_to_template(jet: Elements) -> types::FunctionTemplate { + types::FunctionTemplate::simple( + jet.to_string(), + jet::source_type(jet) + .iter() + .map(|item| format!("{item}")) + .collect::>(), + jet::target_type(jet).to_string().as_str(), + documentation(jet), + ) +} + +// copied from https://github.com/BlockstreamResearch/SimplicityHL/blob/master/codegen/src/jet.rs +#[allow(warnings)] #[rustfmt::skip] pub fn documentation(jet: Elements) -> &'static str { match jet { @@ -35,7 +54,7 @@ pub fn documentation(jet: Elements) -> &'static str { Elements::Eq256 => "Check if two 256-bit values are equal.", Elements::FullLeftShift8_1 => "Helper for left-shifting bits. The bits are shifted from a 1-bit value into a 8-bit value. Return the shifted value and the 1 bit that was shifted out.", Elements::FullLeftShift8_2 => "Helper for left-shifting bits. The bits are shifted from a 2-bit value into a 8-bit value. Return the shifted value and the 2 bits that were shifted out.", - Elements::FullLeftShift8_4 => "Helper for left-shifting bits. The bits are shifted from a 4-bit value into a 8-bit value. Return the shifted value and the 4 bits that were shifted out.", +Elements::FullLeftShift8_4 => "Helper for left-shifting bits. The bits are shifted from a 4-bit value into a 8-bit value. Return the shifted value and the 4 bits that were shifted out.", Elements::FullLeftShift16_1 => "Helper for left-shifting bits. The bits are shifted from a 1-bit value into a 16-bit value. Return the shifted value and the 1 bit that was shifted out.", Elements::FullLeftShift16_2 => "Helper for left-shifting bits. The bits are shifted from a 2-bit value into a 16-bit value. Return the shifted value and the 2 bits that were shifted out.", Elements::FullLeftShift16_4 => "Helper for left-shifting bits. The bits are shifted from a 4-bit value into a 16-bit value. Return the shifted value and the 4 bits that were shifted out.", diff --git a/src/completion/mod.rs b/src/completion/mod.rs new file mode 100644 index 0000000..9facf15 --- /dev/null +++ b/src/completion/mod.rs @@ -0,0 +1,118 @@ +use simplicityhl::parse::Function; + +pub mod builtin; +pub mod jet; +pub mod types; + +use tower_lsp_server::lsp_types::{ + CompletionItem, CompletionItemKind, Documentation, InsertTextFormat, MarkupContent, MarkupKind, +}; + +/// Build and provide `CompletionItem` for Jets and builtin functions. +#[derive(Debug)] +pub struct CompletionProvider { + /// All jets completions. + jets: Vec, + + /// All builtin functions completions. + builtin: Vec, + + /// Modules completions. + modules: Vec, +} + +impl CompletionProvider { + /// Create new `CompletionProvider` with evaluated jets and builtins completions. + pub fn new() -> Self { + let jets_completion = jet::get_jets_completions() + .iter() + .map(template_to_completion) + .collect(); + let builtin_completion = builtin::get_builtin_functions() + .iter() + .map(template_to_completion) + .collect(); + + let modules_completion = [ + ("jet", "Module which contains jets"), + ("param", "Module which contains parameters"), + ("witness", "Module which contains witnesses"), + ] + .iter() + .map(|(module, detail)| module_to_completion((*module).to_string(), (*detail).to_string())) + .collect(); + Self { + jets: jets_completion, + builtin: builtin_completion, + modules: modules_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, String)]) -> Vec { + functions + .iter() + .map(|(func, doc)| { + let template = function_to_template(func, doc); + template_to_completion(&template) + }) + .collect() + } +} + +/// Convert `simplicityhl::parse::Function` to `FunctionTemplate`. +pub fn function_to_template(func: &Function, doc: &String) -> types::FunctionTemplate { + types::FunctionTemplate::simple( + func.name().to_string(), + func.params().iter().map(|item| format!("{item}")).collect(), + match func.ret() { + Some(ret) => format!("{ret}"), + None => "()".to_string(), + }, + doc, + ) +} + +/// Convert `FunctionCompletionTemplate` to `CompletionItem`. +fn template_to_completion(func: &types::FunctionTemplate) -> CompletionItem { + CompletionItem { + label: func.display_name.clone(), + kind: Some(CompletionItemKind::FUNCTION), + detail: Some(func.get_signature()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: func.description.clone(), + })), + insert_text: Some(func.get_insert_text()), + insert_text_format: Some(InsertTextFormat::SNIPPET), + ..Default::default() + } +} + +/// Convert module to `CompletionItem`. +fn module_to_completion(module: String, detail: String) -> CompletionItem { + CompletionItem { + label: module.clone(), + kind: Some(CompletionItemKind::MODULE), + detail: Some(detail), + documentation: None, + insert_text: Some(module), + insert_text_format: Some(InsertTextFormat::PLAIN_TEXT), + ..Default::default() + } +} diff --git a/src/completion/types.rs b/src/completion/types.rs new file mode 100644 index 0000000..c338e4c --- /dev/null +++ b/src/completion/types.rs @@ -0,0 +1,95 @@ +/// Template for all functions +#[derive(Debug, Clone)] +pub struct FunctionTemplate { + /// Display name shown in completion list + pub display_name: String, + /// Base name for snippet + pub snippet_base: String, + /// Generic type parameters to include and use with snippet base + pub generics: Vec, + /// Function arguments + pub args: Vec, + /// Return type + pub return_type: String, + /// Documentation + pub description: String, +} + +impl FunctionTemplate { + /// Create a template with generics (currently used only for builtin functions) + pub fn new( + display_name: impl Into, + snippet_base: impl Into, + generics: Vec, + args: Vec, + return_type: impl Into, + description: impl Into, + ) -> Self { + Self { + display_name: display_name.into(), + snippet_base: snippet_base.into(), + generics, + args, + return_type: return_type.into(), + description: description.into(), + } + } + + /// Create a template without generics + pub fn simple( + name: impl Into, + args: Vec, + return_type: impl Into, + description: impl Into, + ) -> Self { + let name = name.into(); + Self::new(name.clone(), name, vec![], args, return_type, description) + } + + /// Get snippet for function + pub fn get_snippet_name(&self) -> String { + format!( + "{}::<{}>", + self.snippet_base, + self.generics + .iter() + .enumerate() + .map(|(index, item)| { format!("${{{}:{}}}", index + 1, item) }) + .collect::>() + .join(", ") + ) + } + + /// Get text, which would inserted when completion triggered + pub fn get_insert_text(&self) -> String { + format!( + "{}({})", + if self.generics.is_empty() { + self.snippet_base.clone() + } else { + self.get_snippet_name() + }, + self.args + .iter() + .enumerate() + .map(|(index, item)| { + format!("${{{}:{}}}", index + 1 + self.generics.len(), item) + }) + .collect::>() + .join(", ") + ) + } + + /// Get signature text for function, which would show in `detail` field + pub fn get_signature(&self) -> String { + format!( + "fn({}) -> {}", + self.args.join(", "), + if self.return_type.is_empty() { + "()".to_string() + } else { + self.return_type.clone() + } + ) + } +} diff --git a/src/main.rs b/src/main.rs index d17b1a9..15f18f0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,6 @@ -#![warn(clippy::all, clippy::pedantic)] - mod backend; mod completion; -mod jet; +mod utils; use backend::Backend; use tower_lsp_server::{LspService, Server}; diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..ebd92bb --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,72 @@ +use std::num::NonZeroUsize; + +use tower_lsp_server::jsonrpc::{Error, Result}; +use tower_lsp_server::lsp_types; + +fn position_le(a: &simplicityhl::error::Position, b: &simplicityhl::error::Position) -> bool { + (a.line < b.line) || (a.line == b.line && a.col <= b.col) +} + +fn position_ge(a: &simplicityhl::error::Position, b: &simplicityhl::error::Position) -> bool { + (a.line > b.line) || (a.line == b.line && a.col >= b.col) +} + +pub fn span_contains(a: &simplicityhl::error::Span, b: &simplicityhl::error::Span) -> bool { + position_le(&a.start, &b.start) && position_ge(&a.end, &b.end) +} + +/// Convert [`simplicityhl::error::Span`] to [`tower_lsp_server::lsp_types::Position`] +/// +/// Converting is required because `simplicityhl::error::Span` using their own versions of `Position`, +/// which contains non-zero column and line, so they are always starts with one. +/// `Position` required for diagnostic starts with zero +pub fn span_to_positions( + span: &simplicityhl::error::Span, +) -> Result<(lsp_types::Position, lsp_types::Position)> { + let start_line = u32::try_from(span.start.line.get()) + .map_err(|e| Error::invalid_params(format!("line overflow: {e}")))?; + let start_col = u32::try_from(span.start.col.get()) + .map_err(|e| Error::invalid_params(format!("col overflow: {e}")))?; + let end_line = u32::try_from(span.end.line.get()) + .map_err(|e| Error::invalid_params(format!("line overflow: {e}")))?; + let end_col = u32::try_from(span.end.col.get()) + .map_err(|e| Error::invalid_params(format!("col overflow: {e}")))?; + + Ok(( + lsp_types::Position { + line: start_line - 1, + character: start_col - 1, + }, + lsp_types::Position { + line: end_line - 1, + character: end_col - 1, + }, + )) +} + +/// Convert [`tower_lsp_server::lsp_types::Position`] to [`simplicityhl::error::Span`] +pub fn positions_to_span( + positions: (lsp_types::Position, lsp_types::Position), +) -> Result { + let start_line = NonZeroUsize::new((positions.0.line + 1) as usize) + .ok_or_else(|| Error::invalid_params("start line must be non-zero".to_string()))?; + + let start_col = NonZeroUsize::new((positions.0.character + 1) as usize) + .ok_or_else(|| Error::invalid_params("start column must be non-zero".to_string()))?; + + let end_line = NonZeroUsize::new((positions.1.line + 1) as usize) + .ok_or_else(|| Error::invalid_params("end line must be non-zero".to_string()))?; + + let end_col = NonZeroUsize::new((positions.1.character + 1) as usize) + .ok_or_else(|| Error::invalid_params("end column must be non-zero".to_string()))?; + Ok(simplicityhl::error::Span { + start: simplicityhl::error::Position { + line: start_line, + col: start_col, + }, + end: simplicityhl::error::Position { + line: end_line, + col: end_col, + }, + }) +}