Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ node_modules/

# VSCode extension package
*.vsix

# macOS
.DS_Store
431 changes: 408 additions & 23 deletions lsp/src/backend.rs

Large diffs are not rendered by default.

254 changes: 253 additions & 1 deletion lsp/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ use std::num::NonZeroUsize;

use miniscript::iter::TreeLike;

use crate::completion;
use crate::error::LspError;
use ropey::Rope;
use simplicityhl::parse::{self, CallName};
use tower_lsp_server::lsp_types;
use tower_lsp_server::lsp_types::{
self, MarkupContent, MarkupKind, ParameterInformation, ParameterLabel, SignatureInformation,
};

fn position_le(a: &simplicityhl::error::Position, b: &simplicityhl::error::Position) -> bool {
(a.line < b.line) || (a.line == b.line && a.col <= b.col)
Expand Down Expand Up @@ -234,6 +237,188 @@ pub fn find_all_references<'a>(
.collect::<Result<Vec<_>, LspError>>()
}

/// Find the position of a key in the JSON text
pub fn find_key_position(text: &str, key: &str) -> Option<lsp_types::Position> {
let search = format!("\"{}\"", key);
for (line_num, line) in text.lines().enumerate() {
if let Some(col) = line.find(&search) {
return Some(lsp_types::Position::new(line_num as u32, col as u32));
}
}
None
}

/// Find function call context from the current line.
/// Returns (function_name, active_parameter_index) if inside a function call.
pub fn find_function_call_context(line: &str) -> Option<(String, u32)> {
let mut paren_depth = 0;
let mut bracket_depth = 0;
let mut angle_depth = 0;
let mut last_open_paren = None;
let mut comma_count = 0;

// Scan from the end to find the innermost unclosed function call
for (i, ch) in line.chars().rev().enumerate() {
let pos = line.len() - 1 - i;
match ch {
')' => paren_depth += 1,
'(' => {
if paren_depth > 0 {
paren_depth -= 1;
} else {
// Found unclosed '(' - this is our function call
last_open_paren = Some(pos);
break;
}
}
']' => bracket_depth += 1,
'[' => {
if bracket_depth > 0 {
bracket_depth -= 1;
}
}
'>' => angle_depth += 1,
'<' => {
if angle_depth > 0 {
angle_depth -= 1;
}
}
',' if paren_depth == 0 && bracket_depth == 0 && angle_depth == 0 => {
comma_count += 1;
}
_ => {}
}
}

let open_paren_pos = last_open_paren?;

// Extract function name before the '('
let before_paren = &line[..open_paren_pos];
let func_name = extract_function_name(before_paren)?;

Some((func_name, comma_count))
}

/// Extract function name from text before an opening parenthesis.
/// Handles patterns like: `func_name`, `jet::add_32`, `fold::<f, 8>`
pub fn extract_function_name(text: &str) -> Option<String> {
let trimmed = text.trim_end();

// Skip generic parameters if present (e.g., `fold::<f, 8>`)
let without_generics = if trimmed.ends_with('>') {
let mut depth = 0;
let mut start = None;
for (i, ch) in trimmed.chars().rev().enumerate() {
match ch {
'>' => depth += 1,
'<' => {
depth -= 1;
if depth == 0 {
start = Some(trimmed.len() - 1 - i);
break;
}
}
_ => {}
}
}
match start {
Some(pos) => {
let before = &trimmed[..pos];
// Remove the `::` before `<` if present
before.strip_suffix("::").unwrap_or(before)
}
None => trimmed,
}
} else {
trimmed
};

// Now find the function name - it should be an identifier possibly with `::`
let mut name_chars = Vec::new();

for ch in without_generics.chars().rev() {
if ch.is_alphanumeric() || ch == '_' || ch == ':' {
name_chars.push(ch);
} else {
break;
}
}

if name_chars.is_empty() {
return None;
}

name_chars.reverse();
let name: String = name_chars.into_iter().collect();

// Clean up leading colons
let cleaned = name.trim_start_matches(':');
if cleaned.is_empty() {
None
} else {
Some(cleaned.to_string())
}
}

/// Create SignatureInformation from a FunctionTemplate.
pub fn create_signature_info(
template: &completion::types::FunctionTemplate,
) -> SignatureInformation {
let params: Vec<ParameterInformation> = template
.args
.iter()
.map(|arg| ParameterInformation {
label: ParameterLabel::Simple(arg.clone()),
documentation: None,
})
.collect();

let signature_label = format!(
"fn {}({}) -> {}",
template.display_name,
template.args.join(", "),
template.return_type
);

SignatureInformation {
label: signature_label,
documentation: if template.description.is_empty() {
None
} else {
Some(lsp_types::Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: template.description.clone(),
}))
},
parameters: Some(params),
active_parameter: None,
}
}

/// Find signature for builtin functions.
pub fn find_builtin_signature(name: &str) -> Option<SignatureInformation> {
use simplicityhl::str::AliasName;
use simplicityhl::types::AliasedType;

let ty = AliasedType::from(AliasName::from_str_unchecked("T"));

// Match common builtin function names
let call_name = match name {
"unwrap_left" => Some(CallName::UnwrapLeft(ty.clone())),
"unwrap_right" => Some(CallName::UnwrapRight(ty.clone())),
"unwrap" => Some(CallName::Unwrap),
"is_none" => Some(CallName::IsNone(ty.clone())),
"assert!" => Some(CallName::Assert),
"panic!" => Some(CallName::Panic),
"dbg!" => Some(CallName::Debug),
_ => None,
};

let call_name = call_name?;
let template = completion::builtin::match_callname(&call_name)?;
Some(create_signature_info(&template))
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -259,4 +444,71 @@ mod tests {
let result = get_comments_from_lines(0, &text);
assert_eq!(result, "");
}

#[test]
fn test_extract_function_name() {
// Simple function name
assert_eq!(extract_function_name("foo"), Some("foo".to_string()));
assert_eq!(
extract_function_name("my_func"),
Some("my_func".to_string())
);

// With module prefix
assert_eq!(
extract_function_name("jet::add_32"),
Some("jet::add_32".to_string())
);

// With generic parameters
assert_eq!(
extract_function_name("fold::<f, 8>"),
Some("fold".to_string())
);
assert_eq!(
extract_function_name("unwrap_left::<u8>"),
Some("unwrap_left".to_string())
);

// With leading whitespace/expressions
assert_eq!(
extract_function_name("let x = foo"),
Some("foo".to_string())
);

// Empty input
assert_eq!(extract_function_name(""), None);
}

#[test]
fn test_find_function_call_context() {
// Simple function call
assert_eq!(
find_function_call_context("foo("),
Some(("foo".to_string(), 0))
);
assert_eq!(
find_function_call_context("foo(a, "),
Some(("foo".to_string(), 1))
);
assert_eq!(
find_function_call_context("foo(a, b, "),
Some(("foo".to_string(), 2))
);

// Nested function calls
assert_eq!(
find_function_call_context("outer(inner(x), "),
Some(("outer".to_string(), 1))
);

// With module prefix
assert_eq!(
find_function_call_context("jet::add_32(a, "),
Some(("jet::add_32".to_string(), 1))
);

// No function call
assert_eq!(find_function_call_context("let x = 5"), None);
}
}
18 changes: 18 additions & 0 deletions vscode/icons/icon-theme.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"iconDefinitions": {
"simf": {
"iconPath": "./simf.svg"
},
"wit": {
"iconPath": "./wit.svg"
}
},
"fileExtensions": {
"simf": "simf",
"wit": "wit"
},
"languageIds": {
"simplicityhl": "simf",
"simplicityhl-witness": "wit"
}
}
8 changes: 8 additions & 0 deletions vscode/icons/simf.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions vscode/icons/wit.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading