From d3c3b3cedbf5ca63b348f61e3cb6fc664a1cccd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Thu, 5 Jun 2025 15:45:25 -0300 Subject: [PATCH 01/17] feat: initial approximation to hover --- src/lib.rs | 16 ++++++ src/server.rs | 145 ++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 138 insertions(+), 23 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 760857d..60acadc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,6 +62,22 @@ pub fn char_index_to_line_col(rope: &Rope, idx: usize) -> (usize, usize) { (line, col) } +pub fn position_to_offset(text: &str, position: Position) -> usize { + let mut offset = 0; + for (line_num, line) in text.lines().enumerate() { + if line_num == position.line as usize { + offset += position.character.min(line.len() as u32) as usize; + break; + } + offset += line.len() + 1; + } + offset +} + +pub fn span_contains(span: &tx3_lang::ast::Span, offset: usize) -> bool { + offset >= span.start && offset < span.end +} + pub fn span_to_lsp_range(rope: &Rope, loc: &tx3_lang::ast::Span) -> Range { let (start_line, start_col) = char_index_to_line_col(rope, loc.start); let (end_line, end_col) = char_index_to_line_col(rope, loc.end); diff --git a/src/server.rs b/src/server.rs index 44feac2..23f8231 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,7 +1,7 @@ use serde_json::Value; use tower_lsp::{jsonrpc::Result, lsp_types::*, LanguageServer}; -use crate::{cmds, span_to_lsp_range, Context}; +use crate::{cmds, position_to_offset, span_contains, span_to_lsp_range, Context}; #[tower_lsp::async_trait] impl LanguageServer for Context { @@ -58,31 +58,130 @@ impl LanguageServer for Context { } async fn hover(&self, params: HoverParams) -> Result> { - // Get the position where the user is hovering + let uri = ¶ms.text_document_position_params.text_document.uri; let position = params.text_document_position_params.position; - // Here you would typically: - // 1. Parse the document to identify the symbol at the hover position - // 2. Look up information about that symbol - // 3. Return a Hover object with the information + let document = self.documents.get(uri); + if let Some(document) = document { + let text = document.value().to_string(); - // For now, let's return a simple example hover - Ok(Some(Hover { - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::Markdown, - value: "This is a symbol hover example".to_string(), - }), - range: Some(Range { - start: Position { - line: position.line, - character: position.character, - }, - end: Position { - line: position.line, - character: position.character + 1, - }, - }), - })) + let ast = match tx3_lang::parsing::parse_string(text.as_str()) { + Ok(ast) => ast, + Err(_) => return Ok(None), + }; + + let offset = position_to_offset(&text, position); + + for party in &ast.parties { + if span_contains(&party.span, offset) { + return Ok(Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: format!( + "**Party**: `{}`\n\nA party in the transaction. It can be an address for a script or a wallet.", + party.name + ), + }), + range: Some(span_to_lsp_range(document.value(), &party.span)), + })); + } + } + + for policy in &ast.policies { + if span_contains(&policy.span, offset) { + return Ok(Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: format!("**Policy**: `{}`\n\nA policy definition.", policy.name), + }), + range: Some(span_to_lsp_range(document.value(), &policy.span)), + })); + } + } + + for tx in &ast.txs { + for input in &tx.inputs { + if span_contains(&input.span, offset) { + return Ok(Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: format!("**Input**: `{}`\n\nTransaction input.", input.name), + }), + range: Some(span_to_lsp_range(document.value(), &input.span)), + })); + } + } + + for output in &tx.outputs { + if span_contains(&output.span, offset) { + let default_output = "output".to_string(); + let name = output.name.as_ref().unwrap_or(&default_output); + return Ok(Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: format!("**Output**: `{}`\n\nTransaction output.", name), + }), + range: Some(span_to_lsp_range(document.value(), &output.span)), + })); + } + } + + if span_contains(&tx.parameters.span, offset) { + for param in &tx.parameters.parameters { + return Ok(Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: format!( + "**Parameter**: `{}`\n\n**Type**: `{:?}`", + param.name, param.r#type + ), + }), + range: Some(span_to_lsp_range(document.value(), &tx.parameters.span)), + })); + } + } + + if span_contains(&tx.span, offset) { + let mut hover_text = format!("**Transaction**: `{}`\n\n", tx.name); + + if !tx.parameters.parameters.is_empty() { + hover_text.push_str("**Parameters**:\n"); + for param in &tx.parameters.parameters { + hover_text + .push_str(&format!("- `{}`: `{:?}`\n", param.name, param.r#type)); + } + hover_text.push_str("\n"); + } + + if !tx.inputs.is_empty() { + hover_text.push_str("**Inputs**:\n"); + for input in &tx.inputs { + hover_text.push_str(&format!("- `{}`\n", input.name)); + } + hover_text.push_str("\n"); + } + + if !tx.outputs.is_empty() { + hover_text.push_str("**Outputs**:\n"); + for output in &tx.outputs { + let default_output = "output".to_string(); + let name = output.name.as_ref().unwrap_or(&default_output); + hover_text.push_str(&format!("- `{}`\n", name)); + } + } + + return Ok(Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: hover_text, + }), + range: Some(span_to_lsp_range(document.value(), &tx.span)), + })); + } + } + } + + Ok(None) } // TODO: Add error handling and improve From 921e9d7f93414157685d9c0a6a9963299d63cfac Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Fri, 6 Jun 2025 15:13:38 -0300 Subject: [PATCH 02/17] feat: implement get_identifier_at_position --- src/lib.rs | 26 ++++++++++++++++++++++++ src/server.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 60acadc..16813a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,6 +74,32 @@ pub fn position_to_offset(text: &str, position: Position) -> usize { offset } +// REVIEW +pub fn get_identifier_at_position(text: &str, offset: usize) -> Option { + // Find word boundaries around the offset + let chars: Vec = text.chars().collect(); + + if offset >= chars.len() { + return None; + } + + let mut start = offset; + while start > 0 && (chars[start - 1].is_alphanumeric() || chars[start - 1] == '_') { + start -= 1; + } + + let mut end = offset; + while end < chars.len() && (chars[end].is_alphanumeric() || chars[end] == '_') { + end += 1; + } + + if start < end { + Some(chars[start..end].iter().collect()) + } else { + None + } +} + pub fn span_contains(span: &tx3_lang::ast::Span, offset: usize) -> bool { offset >= span.start && offset < span.end } diff --git a/src/server.rs b/src/server.rs index 23f8231..44d13ed 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,7 +1,9 @@ use serde_json::Value; use tower_lsp::{jsonrpc::Result, lsp_types::*, LanguageServer}; -use crate::{cmds, position_to_offset, span_contains, span_to_lsp_range, Context}; +use crate::{ + cmds, get_identifier_at_position, position_to_offset, span_contains, span_to_lsp_range, Context, +}; #[tower_lsp::async_trait] impl LanguageServer for Context { @@ -46,9 +48,56 @@ impl LanguageServer for Context { async fn goto_definition( &self, - _: GotoDefinitionParams, + params: GotoDefinitionParams, ) -> Result> { - // Return None for now, indicating no definition found + let uri = ¶ms.text_document_position_params.text_document.uri; + let position = params.text_document_position_params.position; + + let document = self.documents.get(uri); + if let Some(document) = document { + let text = document.value().to_string(); + + let ast = match tx3_lang::parsing::parse_string(text.as_str()) { + Ok(ast) => ast, + Err(_) => return Ok(None), + }; + + let offset = position_to_offset(&text, position); + + let identifier_at_position = get_identifier_at_position(&text, offset); + + if let Some(identifier) = identifier_at_position { + for party in &ast.parties { + if party.name == identifier { + return Ok(Some(GotoDefinitionResponse::Scalar(Location { + uri: uri.clone(), + range: span_to_lsp_range(document.value(), &party.span), + }))); + } + } + for policy in &ast.policies { + if policy.name == identifier { + return Ok(Some(GotoDefinitionResponse::Scalar(Location { + uri: uri.clone(), + range: span_to_lsp_range(document.value(), &policy.span), + }))); + } + } + for tx in &ast.txs { + if span_contains(&tx.span, offset) { + for param in &tx.parameters.parameters { + if param.name == identifier { + return Ok(Some(GotoDefinitionResponse::Scalar(Location { + uri: uri.clone(), + range: span_to_lsp_range(document.value(), &tx.parameters.span), + }))); + } + } + } + } + } + } + Ok(None) } From b901d5576c31f12dbac8b6781fe059da267efb30 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Fri, 6 Jun 2025 15:26:43 -0300 Subject: [PATCH 03/17] feat: enhance goto definition to include parameters, inputs, outputs, and references --- src/server.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/server.rs b/src/server.rs index 44d13ed..264fe41 100644 --- a/src/server.rs +++ b/src/server.rs @@ -75,6 +75,7 @@ impl LanguageServer for Context { }))); } } + for policy in &ast.policies { if policy.name == identifier { return Ok(Some(GotoDefinitionResponse::Scalar(Location { @@ -83,8 +84,10 @@ impl LanguageServer for Context { }))); } } + for tx in &ast.txs { if span_contains(&tx.span, offset) { + // Check if it's a parameter for param in &tx.parameters.parameters { if param.name == identifier { return Ok(Some(GotoDefinitionResponse::Scalar(Location { @@ -93,6 +96,35 @@ impl LanguageServer for Context { }))); } } + + for input in &tx.inputs { + if input.name == identifier { + return Ok(Some(GotoDefinitionResponse::Scalar(Location { + uri: uri.clone(), + range: span_to_lsp_range(document.value(), &input.span), + }))); + } + } + + for output in &tx.outputs { + if let Some(output_name) = &output.name { + if output_name == &identifier { + return Ok(Some(GotoDefinitionResponse::Scalar(Location { + uri: uri.clone(), + range: span_to_lsp_range(document.value(), &output.span), + }))); + } + } + } + + for reference in &tx.references { + if reference.name == identifier { + return Ok(Some(GotoDefinitionResponse::Scalar(Location { + uri: uri.clone(), + range: span_to_lsp_range(document.value(), &reference.span), + }))); + } + } } } } From eee4e28f1e404402c5aa3e8d18ab35e8592f326f Mon Sep 17 00:00:00 2001 From: FranZavalla Date: Fri, 6 Jun 2025 16:51:41 -0300 Subject: [PATCH 04/17] remove comments and added TODO to change the `goto_definition` implementation --- src/lib.rs | 5 ++--- src/server.rs | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 16813a0..c539541 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,9 +74,8 @@ pub fn position_to_offset(text: &str, position: Position) -> usize { offset } -// REVIEW +// TODO: Find the smallest span at the offset in the AST pub fn get_identifier_at_position(text: &str, offset: usize) -> Option { - // Find word boundaries around the offset let chars: Vec = text.chars().collect(); if offset >= chars.len() { @@ -199,4 +198,4 @@ impl Context { Err(e) => vec![parse_error_to_diagnostic(&rope, &e)], } } -} +} \ No newline at end of file diff --git a/src/server.rs b/src/server.rs index 264fe41..4cce351 100644 --- a/src/server.rs +++ b/src/server.rs @@ -87,7 +87,6 @@ impl LanguageServer for Context { for tx in &ast.txs { if span_contains(&tx.span, offset) { - // Check if it's a parameter for param in &tx.parameters.parameters { if param.name == identifier { return Ok(Some(GotoDefinitionResponse::Scalar(Location { From f902111c24d5db7f978fdaadea10827fd92c20fc Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Mon, 9 Jun 2025 15:57:35 -0300 Subject: [PATCH 05/17] feat: add semantic tokens support and implement token collection logic --- src/lib.rs | 183 +++++++++++++++++++++++++++++++++++++++++++++++++- src/server.rs | 80 ++++++++++++++++++++++ 2 files changed, 262 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index c539541..4aa00d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -158,6 +158,187 @@ pub struct Context { } impl Context { + fn collect_semantic_tokens( + &self, + ast: &tx3_lang::ast::Program, + rope: &Rope, + ) -> Vec { + // Token type indices based on the legend order + const TOKEN_KEYWORD: u32 = 0; + const TOKEN_TYPE: u32 = 1; + const TOKEN_PARAMETER: u32 = 2; + const TOKEN_VARIABLE: u32 = 3; + const TOKEN_FUNCTION: u32 = 4; + const TOKEN_CLASS: u32 = 5; + const TOKEN_PROPERTY: u32 = 6; + const TOKEN_PARTY: u32 = 7; + const TOKEN_POLICY: u32 = 8; + const TOKEN_TRANSACTION: u32 = 9; + const TOKEN_INPUT: u32 = 10; + const TOKEN_OUTPUT: u32 = 11; + const TOKEN_REFERENCE: u32 = 12; + + // Token modifiers + const MOD_DECLARATION: u32 = 1 << 0; + const MOD_DEFINITION: u32 = 1 << 1; + + #[derive(Debug, Clone)] + struct TokenInfo { + range: Range, + token_type: u32, + token_modifiers: u32, + } + + let mut token_infos: Vec = Vec::new(); + + // TODO: Use the span function to get the identifier ranges + let extract_identifier_after_keyword = + |span: &tx3_lang::ast::Span, keyword: &str, identifier: &str| -> Option { + let start_char = span.start; + let end_char = span.end; + + if start_char >= end_char { + return None; + } + + let text_slice = rope.slice(start_char..end_char); + let text = text_slice.to_string(); + + // Find keyword first + if let Some(keyword_pos) = text.find(keyword) { + // Look for the identifier after the keyword + let after_keyword = &text[keyword_pos + keyword.len()..]; + if let Some(id_pos) = after_keyword.find(identifier) { + let absolute_id_start = start_char + keyword_pos + keyword.len() + id_pos; + let absolute_id_end = absolute_id_start + identifier.len(); + + return Some(span_to_lsp_range( + rope, + &tx3_lang::ast::Span::new(absolute_id_start, absolute_id_end), + )); + } + } + + // If we can't find keyword, just try to find the identifier + if let Some(id_pos) = text.find(identifier) { + let absolute_id_start = start_char + id_pos; + let absolute_id_end = absolute_id_start + identifier.len(); + + return Some(span_to_lsp_range( + rope, + &tx3_lang::ast::Span::new(absolute_id_start, absolute_id_end), + )); + } + + None + }; + + // Parties + for party in &ast.parties { + if let Some(range) = extract_identifier_after_keyword(&party.span, "party", &party.name) + { + token_infos.push(TokenInfo { + range, + token_type: TOKEN_PARTY, + token_modifiers: MOD_DECLARATION, + }); + } + } + + // Policies + for policy in &ast.policies { + if let Some(range) = + extract_identifier_after_keyword(&policy.span, "policy", &policy.name) + { + token_infos.push(TokenInfo { + range, + token_type: TOKEN_POLICY, + token_modifiers: MOD_DECLARATION, + }); + } + } + + // Types + for type_def in &ast.types { + if let Some(range) = + extract_identifier_after_keyword(&type_def.span, "type", &type_def.name) + { + token_infos.push(TokenInfo { + range, + token_type: TOKEN_TYPE, + token_modifiers: MOD_DECLARATION, + }); + } + } + + // Assets + for asset in &ast.assets { + if let Some(range) = extract_identifier_after_keyword(&asset.span, "asset", &asset.name) + { + token_infos.push(TokenInfo { + range, + token_type: TOKEN_CLASS, + token_modifiers: MOD_DECLARATION, + }); + } + } + + // Transactions + for tx in &ast.txs { + if let Some(range) = extract_identifier_after_keyword(&tx.span, "tx", &tx.name) { + token_infos.push(TokenInfo { + range, + token_type: TOKEN_TRANSACTION, + token_modifiers: MOD_DECLARATION, + }); + } + } + + // Sort tokens by position + token_infos.sort_by(|a, b| match a.range.start.line.cmp(&b.range.start.line) { + std::cmp::Ordering::Equal => a.range.start.character.cmp(&b.range.start.character), + other => other, + }); + + // Remove duplicates + token_infos.dedup_by(|a, b| a.range.start == b.range.start && a.range.end == b.range.end); + + // Convert to semantic tokens with deltas + let mut semantic_tokens = Vec::new(); + let mut prev_line = 0; + let mut prev_start = 0; + + for token_info in token_infos { + let line = token_info.range.start.line; + let start = token_info.range.start.character; + let length = token_info.range.end.character.saturating_sub(start); + + if length == 0 { + continue; + } + + let delta_line = line.saturating_sub(prev_line); + let delta_start = if delta_line == 0 { + start.saturating_sub(prev_start) + } else { + start + }; + + semantic_tokens.push(SemanticToken { + delta_line, + delta_start, + length, + token_type: token_info.token_type, + token_modifiers_bitset: token_info.token_modifiers, + }); + + prev_line = line; + prev_start = start; + } + + semantic_tokens + } + pub fn new_for_client(client: Client) -> Self { Self { client, @@ -198,4 +379,4 @@ impl Context { Err(e) => vec![parse_error_to_diagnostic(&rope, &e)], } } -} \ No newline at end of file +} diff --git a/src/server.rs b/src/server.rs index 4cce351..651f2c8 100644 --- a/src/server.rs +++ b/src/server.rs @@ -20,6 +20,39 @@ impl LanguageServer for Context { text_document_sync: Some(TextDocumentSyncCapability::Kind( TextDocumentSyncKind::FULL, )), + semantic_tokens_provider: Some( + SemanticTokensServerCapabilities::SemanticTokensOptions( + SemanticTokensOptions { + work_done_progress_options: WorkDoneProgressOptions::default(), + legend: SemanticTokensLegend { + token_types: vec![ + SemanticTokenType::KEYWORD, + SemanticTokenType::TYPE, + SemanticTokenType::PARAMETER, + SemanticTokenType::VARIABLE, + SemanticTokenType::FUNCTION, + SemanticTokenType::CLASS, + SemanticTokenType::PROPERTY, + // Custom token types for tx3 + SemanticTokenType::new("party"), + SemanticTokenType::new("policy"), + SemanticTokenType::new("transaction"), + SemanticTokenType::new("input"), + SemanticTokenType::new("output"), + SemanticTokenType::new("reference"), + ], + token_modifiers: vec![ + SemanticTokenModifier::DECLARATION, + SemanticTokenModifier::DEFINITION, + SemanticTokenModifier::READONLY, + SemanticTokenModifier::STATIC, + ], + }, + range: Some(true), + full: Some(SemanticTokensFullOptions::Bool(true)), + }, + ), + ), execute_command_provider: Some(ExecuteCommandOptions { commands: vec!["generate-tir".to_string(), "generate-ast".to_string()], work_done_progress_options: WorkDoneProgressOptions { @@ -46,6 +79,53 @@ impl LanguageServer for Context { Ok(Some(CompletionResponse::Array(vec![]))) } + // REVIEW + async fn semantic_tokens_full( + &self, + params: SemanticTokensParams, + ) -> Result> { + let uri = ¶ms.text_document.uri; + let document = self.documents.get(uri); + + if let Some(document) = document { + let text = document.value().to_string(); + let rope = document.value(); + + let ast = match tx3_lang::parsing::parse_string(text.as_str()) { + Ok(ast) => ast, + Err(_) => return Ok(None), + }; + + let tokens = self.collect_semantic_tokens(&ast, rope); + + Ok(Some(SemanticTokensResult::Tokens(SemanticTokens { + result_id: None, + data: tokens, + }))) + } else { + Ok(None) + } + } + + async fn semantic_tokens_range( + &self, + params: SemanticTokensRangeParams, + ) -> Result> { + // TODO: optimize this for the specific range + let full_params = SemanticTokensParams { + text_document: params.text_document, + work_done_progress_params: params.work_done_progress_params, + partial_result_params: params.partial_result_params, + }; + + self.semantic_tokens_full(full_params).await.map(|result| { + result.map(|tokens| match tokens { + SemanticTokensResult::Tokens(t) => SemanticTokensRangeResult::Tokens(t), + SemanticTokensResult::Partial(p) => SemanticTokensRangeResult::Partial(p), + }) + }) + } + async fn goto_definition( &self, params: GotoDefinitionParams, From 6350e25eefde936303663d624b20d9cf4af6d7f4 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Fri, 13 Jun 2025 15:39:39 -0300 Subject: [PATCH 06/17] feat: add visitor module with symbol lookup functionality --- src/lib.rs | 1 + src/server.rs | 22 ++- src/visitor.rs | 491 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 505 insertions(+), 9 deletions(-) create mode 100644 src/visitor.rs diff --git a/src/lib.rs b/src/lib.rs index 4aa00d2..46c7217 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ use tx3_lang::Protocol; mod cmds; mod server; +mod visitor; #[derive(Error, Debug)] pub enum Error { diff --git a/src/server.rs b/src/server.rs index 651f2c8..8718c31 100644 --- a/src/server.rs +++ b/src/server.rs @@ -2,7 +2,9 @@ use serde_json::Value; use tower_lsp::{jsonrpc::Result, lsp_types::*, LanguageServer}; use crate::{ - cmds, get_identifier_at_position, position_to_offset, span_contains, span_to_lsp_range, Context, + cmds, position_to_offset, span_contains, span_to_lsp_range, + visitor::{find_symbol_in_program, SymbolAtOffset}, + Context, }; #[tower_lsp::async_trait] @@ -144,11 +146,13 @@ impl LanguageServer for Context { let offset = position_to_offset(&text, position); - let identifier_at_position = get_identifier_at_position(&text, offset); + if let Some(symbol) = find_symbol_in_program(&ast, offset) { + let identifier = match symbol { + SymbolAtOffset::Identifier(x) => x, + }; - if let Some(identifier) = identifier_at_position { for party in &ast.parties { - if party.name == identifier { + if party.name == identifier.value { return Ok(Some(GotoDefinitionResponse::Scalar(Location { uri: uri.clone(), range: span_to_lsp_range(document.value(), &party.span), @@ -157,7 +161,7 @@ impl LanguageServer for Context { } for policy in &ast.policies { - if policy.name == identifier { + if policy.name == identifier.value { return Ok(Some(GotoDefinitionResponse::Scalar(Location { uri: uri.clone(), range: span_to_lsp_range(document.value(), &policy.span), @@ -168,7 +172,7 @@ impl LanguageServer for Context { for tx in &ast.txs { if span_contains(&tx.span, offset) { for param in &tx.parameters.parameters { - if param.name == identifier { + if param.name == identifier.value { return Ok(Some(GotoDefinitionResponse::Scalar(Location { uri: uri.clone(), range: span_to_lsp_range(document.value(), &tx.parameters.span), @@ -177,7 +181,7 @@ impl LanguageServer for Context { } for input in &tx.inputs { - if input.name == identifier { + if input.name == identifier.value { return Ok(Some(GotoDefinitionResponse::Scalar(Location { uri: uri.clone(), range: span_to_lsp_range(document.value(), &input.span), @@ -187,7 +191,7 @@ impl LanguageServer for Context { for output in &tx.outputs { if let Some(output_name) = &output.name { - if output_name == &identifier { + if output_name == &identifier.value { return Ok(Some(GotoDefinitionResponse::Scalar(Location { uri: uri.clone(), range: span_to_lsp_range(document.value(), &output.span), @@ -197,7 +201,7 @@ impl LanguageServer for Context { } for reference in &tx.references { - if reference.name == identifier { + if reference.name == identifier.value { return Ok(Some(GotoDefinitionResponse::Scalar(Location { uri: uri.clone(), range: span_to_lsp_range(document.value(), &reference.span), diff --git a/src/visitor.rs b/src/visitor.rs new file mode 100644 index 0000000..d591394 --- /dev/null +++ b/src/visitor.rs @@ -0,0 +1,491 @@ +#[derive(Debug)] +pub enum SymbolAtOffset<'a> { + Identifier(&'a tx3_lang::ast::Identifier), + // add more symbol variants if needed +} + +pub fn find_symbol_in_program<'a>( + program: &'a tx3_lang::ast::Program, + offset: usize, +) -> Option> { + for tx in &program.txs { + if let Some(sym) = visit_tx_def(tx, offset) { + return Some(sym); + } + } + for asset in &program.assets { + if let Some(sym) = visit_asset_def(asset, offset) { + return Some(sym); + } + } + for ty in &program.types { + if let Some(sym) = visit_type_def(ty, offset) { + return Some(sym); + } + } + for party in &program.parties { + if let Some(sym) = visit_party_def(party, offset) { + return Some(sym); + } + } + for policy in &program.policies { + if let Some(sym) = visit_policy_def(policy, offset) { + return Some(sym); + } + } + None +} + +fn visit_tx_def<'a>(tx: &'a tx3_lang::ast::TxDef, offset: usize) -> Option> { + if let Some(sym) = visit_parameter_list(&tx.parameters, offset) { + return Some(sym); + } + for input in &tx.inputs { + if let Some(sym) = visit_input_block(input, offset) { + return Some(sym); + } + } + for output in &tx.outputs { + if let Some(sym) = visit_output_block(output, offset) { + return Some(sym); + } + } + for mint in &tx.mints { + if let Some(sym) = visit_mint_block(mint, offset) { + return Some(sym); + } + } + for ref_block in &tx.references { + if let Some(sym) = visit_reference_block(ref_block, offset) { + return Some(sym); + } + } + for adhoc in &tx.adhoc { + if let Some(sym) = visit_chain_specific_block(adhoc, offset) { + return Some(sym); + } + } + for col in &tx.collateral { + if let Some(sym) = visit_collateral_block(col, offset) { + return Some(sym); + } + } + if let Some(signers) = &tx.signers { + if let Some(sym) = visit_signers_block(signers, offset) { + return Some(sym); + } + } + if let Some(validity) = &tx.validity { + if let Some(sym) = visit_validity_block(validity, offset) { + return Some(sym); + } + } + if let Some(burn) = &tx.burn { + if let Some(sym) = visit_burn_block(burn, offset) { + return Some(sym); + } + } + if let Some(metadata) = &tx.metadata { + if let Some(sym) = visit_metadata_block(metadata, offset) { + return Some(sym); + } + } + None +} + +fn visit_parameter_list<'a>( + params: &'a tx3_lang::ast::ParameterList, + offset: usize, +) -> Option> { + for param in ¶ms.parameters { + if let Some(sym) = visit_type(¶m.r#type, offset) { + return Some(sym); + } + } + None +} + +fn visit_type<'a>(ty: &'a tx3_lang::ast::Type, offset: usize) -> Option> { + match ty { + tx3_lang::ast::Type::Custom(id) => visit_identifier(id, offset), + tx3_lang::ast::Type::List(inner) => visit_type(inner, offset), + _ => None, + } +} + +fn visit_identifier<'a>( + id: &'a tx3_lang::ast::Identifier, + offset: usize, +) -> Option> { + if in_span(&id.span, offset) { + Some(SymbolAtOffset::Identifier(id)) + } else { + None + } +} + +fn visit_input_block<'a>( + input: &'a tx3_lang::ast::InputBlock, + offset: usize, +) -> Option> { + for field in &input.fields { + if let Some(sym) = visit_input_block_field(field, offset) { + return Some(sym); + } + } + None +} + +fn visit_input_block_field<'a>( + field: &'a tx3_lang::ast::InputBlockField, + offset: usize, +) -> Option> { + match field { + tx3_lang::ast::InputBlockField::From(addr) => visit_address_expr(addr, offset), + tx3_lang::ast::InputBlockField::DatumIs(ty) => visit_type(ty, offset), + tx3_lang::ast::InputBlockField::MinAmount(expr) => visit_asset_expr(expr, offset), + tx3_lang::ast::InputBlockField::Redeemer(expr) => visit_data_expr(expr, offset), + tx3_lang::ast::InputBlockField::Ref(expr) => visit_data_expr(expr, offset), + } +} + +fn visit_output_block<'a>( + output: &'a tx3_lang::ast::OutputBlock, + offset: usize, +) -> Option> { + for field in &output.fields { + if let Some(sym) = visit_output_block_field(field, offset) { + return Some(sym); + } + } + None +} + +fn visit_output_block_field<'a>( + field: &'a tx3_lang::ast::OutputBlockField, + offset: usize, +) -> Option> { + match field { + tx3_lang::ast::OutputBlockField::To(addr) => visit_address_expr(addr, offset), + tx3_lang::ast::OutputBlockField::Amount(expr) => visit_asset_expr(expr, offset), + tx3_lang::ast::OutputBlockField::Datum(expr) => visit_data_expr(expr, offset), + } +} + +fn visit_asset_expr<'a>( + expr: &'a tx3_lang::ast::AssetExpr, + offset: usize, +) -> Option> { + match expr { + tx3_lang::ast::AssetExpr::Identifier(id) => visit_identifier(id, offset), + tx3_lang::ast::AssetExpr::StaticConstructor(constr) => { + if let Some(sym) = visit_identifier(&constr.r#type, offset) { + return Some(sym); + } + visit_data_expr(&constr.amount, offset) + } + tx3_lang::ast::AssetExpr::AnyConstructor(constr) => { + if let Some(sym) = visit_data_expr(&constr.policy, offset) { + return Some(sym); + } + if let Some(sym) = visit_data_expr(&constr.asset_name, offset) { + return Some(sym); + } + visit_data_expr(&constr.amount, offset) + } + tx3_lang::ast::AssetExpr::BinaryOp(binop) => { + if let Some(sym) = visit_asset_expr(&binop.left, offset) { + return Some(sym); + } + visit_asset_expr(&binop.right, offset) + } + tx3_lang::ast::AssetExpr::PropertyAccess(pa) => visit_property_access(pa, offset), + } +} + +fn visit_data_expr<'a>( + expr: &'a tx3_lang::ast::DataExpr, + offset: usize, +) -> Option> { + match expr { + tx3_lang::ast::DataExpr::Identifier(id) => visit_identifier(id, offset), + tx3_lang::ast::DataExpr::StructConstructor(sc) => visit_struct_constructor(sc, offset), + tx3_lang::ast::DataExpr::ListConstructor(lc) => { + for el in &lc.elements { + if let Some(sym) = visit_data_expr(el, offset) { + return Some(sym); + } + } + None + } + tx3_lang::ast::DataExpr::PropertyAccess(pa) => visit_property_access(pa, offset), + tx3_lang::ast::DataExpr::BinaryOp(binop) => { + if let Some(sym) = visit_data_expr(&binop.left, offset) { + return Some(sym); + } + visit_data_expr(&binop.right, offset) + } + _ => None, + } +} + +fn visit_struct_constructor<'a>( + sc: &'a tx3_lang::ast::StructConstructor, + offset: usize, +) -> Option> { + if let Some(sym) = visit_identifier(&sc.r#type, offset) { + return Some(sym); + } + visit_variant_case_constructor(&sc.case, offset) +} + +fn visit_variant_case_constructor<'a>( + vc: &'a tx3_lang::ast::VariantCaseConstructor, + offset: usize, +) -> Option> { + if let Some(sym) = visit_identifier(&vc.name, offset) { + return Some(sym); + } + for field in &vc.fields { + if let Some(sym) = visit_record_constructor_field(field, offset) { + return Some(sym); + } + } + if let Some(spread) = &vc.spread { + return visit_data_expr(spread, offset); + } + None +} + +fn visit_record_constructor_field<'a>( + field: &'a tx3_lang::ast::RecordConstructorField, + offset: usize, +) -> Option> { + if let Some(sym) = visit_identifier(&field.name, offset) { + return Some(sym); + } + visit_data_expr(&field.value, offset) +} + +fn visit_property_access<'a>( + pa: &'a tx3_lang::ast::PropertyAccess, + offset: usize, +) -> Option> { + if let Some(sym) = visit_identifier(&pa.object, offset) { + return Some(sym); + } + for id in &pa.path { + if let Some(sym) = visit_identifier(id, offset) { + return Some(sym); + } + } + None +} + +fn visit_reference_block<'a>( + rb: &'a tx3_lang::ast::ReferenceBlock, + offset: usize, +) -> Option> { + visit_data_expr(&rb.r#ref, offset) +} + +fn visit_chain_specific_block<'a>( + _cb: &'a tx3_lang::ast::ChainSpecificBlock, + _offset: usize, +) -> Option> { + None +} + +fn visit_collateral_block<'a>( + cb: &'a tx3_lang::ast::CollateralBlock, + offset: usize, +) -> Option> { + for field in &cb.fields { + match field { + tx3_lang::ast::CollateralBlockField::From(addr) => { + if let Some(sym) = visit_address_expr(addr, offset) { + return Some(sym); + } + } + tx3_lang::ast::CollateralBlockField::MinAmount(expr) => { + if let Some(sym) = visit_asset_expr(expr, offset) { + return Some(sym); + } + } + tx3_lang::ast::CollateralBlockField::Ref(expr) => { + if let Some(sym) = visit_data_expr(expr, offset) { + return Some(sym); + } + } + } + } + None +} + +fn visit_signers_block<'a>( + sb: &'a tx3_lang::ast::SignersBlock, + offset: usize, +) -> Option> { + for signer in &sb.signers { + if let Some(sym) = visit_data_expr(signer, offset) { + return Some(sym); + } + } + None +} + +fn visit_validity_block<'a>( + vb: &'a tx3_lang::ast::ValidityBlock, + offset: usize, +) -> Option> { + for field in &vb.fields { + match field { + tx3_lang::ast::ValidityBlockField::SinceSlot(expr) + | tx3_lang::ast::ValidityBlockField::UntilSlot(expr) => { + if let Some(sym) = visit_data_expr(expr, offset) { + return Some(sym); + } + } + } + } + None +} + +fn visit_burn_block<'a>( + bb: &'a tx3_lang::ast::BurnBlock, + offset: usize, +) -> Option> { + for field in &bb.fields { + match field { + tx3_lang::ast::MintBlockField::Amount(expr) => { + if let Some(sym) = visit_asset_expr(expr, offset) { + return Some(sym); + } + } + tx3_lang::ast::MintBlockField::Redeemer(expr) => { + if let Some(sym) = visit_data_expr(expr, offset) { + return Some(sym); + } + } + } + } + None +} + +fn visit_metadata_block<'a>( + _mb: &'a tx3_lang::ast::MetadataBlock, + _offset: usize, +) -> Option> { + None +} + +fn visit_mint_block<'a>( + mb: &'a tx3_lang::ast::MintBlock, + offset: usize, +) -> Option> { + for field in &mb.fields { + match field { + tx3_lang::ast::MintBlockField::Amount(expr) => { + if let Some(sym) = visit_asset_expr(expr, offset) { + return Some(sym); + } + } + tx3_lang::ast::MintBlockField::Redeemer(expr) => { + if let Some(sym) = visit_data_expr(expr, offset) { + return Some(sym); + } + } + } + } + None +} + +fn visit_asset_def<'a>( + asset: &'a tx3_lang::ast::AssetDef, + offset: usize, +) -> Option> { + if let Some(sym) = visit_data_expr(&asset.policy, offset) { + return Some(sym); + } + if let Some(sym) = visit_data_expr(&asset.asset_name, offset) { + return Some(sym); + } + None +} + +fn visit_type_def<'a>(ty: &'a tx3_lang::ast::TypeDef, offset: usize) -> Option> { + for case in &ty.cases { + if let Some(sym) = visit_variant_case(case, offset) { + return Some(sym); + } + } + None +} + +fn visit_variant_case<'a>( + case: &'a tx3_lang::ast::VariantCase, + offset: usize, +) -> Option> { + for field in &case.fields { + if let Some(sym) = visit_record_field(field, offset) { + return Some(sym); + } + } + None +} + +fn visit_record_field<'a>( + field: &'a tx3_lang::ast::RecordField, + offset: usize, +) -> Option> { + visit_type(&field.r#type, offset) +} + +fn visit_party_def<'a>( + _party: &'a tx3_lang::ast::PartyDef, + _offset: usize, +) -> Option> { + None +} + +fn visit_policy_def<'a>( + policy: &'a tx3_lang::ast::PolicyDef, + offset: usize, +) -> Option> { + match &policy.value { + tx3_lang::ast::PolicyValue::Constructor(constr) => { + for field in &constr.fields { + if let Some(sym) = visit_policy_field(field, offset) { + return Some(sym); + } + } + } + tx3_lang::ast::PolicyValue::Assign(_) => {} + } + None +} + +fn visit_policy_field<'a>( + field: &'a tx3_lang::ast::PolicyField, + offset: usize, +) -> Option> { + match field { + tx3_lang::ast::PolicyField::Hash(expr) => visit_data_expr(expr, offset), + tx3_lang::ast::PolicyField::Script(expr) => visit_data_expr(expr, offset), + tx3_lang::ast::PolicyField::Ref(expr) => visit_data_expr(expr, offset), + } +} + +fn visit_address_expr<'a>( + expr: &'a tx3_lang::ast::AddressExpr, + offset: usize, +) -> Option> { + match expr { + tx3_lang::ast::AddressExpr::Identifier(id) => visit_identifier(id, offset), + _ => None, + } +} + +fn in_span(span: &tx3_lang::ast::Span, offset: usize) -> bool { + span.start <= offset && offset < span.end +} From d39463159ceecbe517995e64df298415853590ff Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Wed, 18 Jun 2025 16:21:20 -0300 Subject: [PATCH 07/17] feat: improve identifier extraction and symbol resolution in visitor and server modules --- src/lib.rs | 190 +++++++++++++++++++++++-------------------------- src/server.rs | 6 +- src/visitor.rs | 7 +- 3 files changed, 99 insertions(+), 104 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 46c7217..c037518 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -191,107 +191,99 @@ impl Context { } let mut token_infos: Vec = Vec::new(); - - // TODO: Use the span function to get the identifier ranges - let extract_identifier_after_keyword = - |span: &tx3_lang::ast::Span, keyword: &str, identifier: &str| -> Option { - let start_char = span.start; - let end_char = span.end; - - if start_char >= end_char { - return None; - } - - let text_slice = rope.slice(start_char..end_char); - let text = text_slice.to_string(); - - // Find keyword first - if let Some(keyword_pos) = text.find(keyword) { - // Look for the identifier after the keyword - let after_keyword = &text[keyword_pos + keyword.len()..]; - if let Some(id_pos) = after_keyword.find(identifier) { - let absolute_id_start = start_char + keyword_pos + keyword.len() + id_pos; - let absolute_id_end = absolute_id_start + identifier.len(); - - return Some(span_to_lsp_range( - rope, - &tx3_lang::ast::Span::new(absolute_id_start, absolute_id_end), - )); + let text = rope.to_string(); + + // Find all identifiers in the program + let mut processed_spans = std::collections::HashSet::new(); + + // We need to scan each position to find symbols using the same mechanism as hover and goto + for offset in 0..text.len() { + if let Some(symbol) = crate::visitor::find_symbol_in_program(ast, offset) { + match symbol { + crate::visitor::SymbolAtOffset::Identifier(identifier) => { + // Skip if we've already processed this exact span + let span_key = (identifier.span.start, identifier.span.end); + if processed_spans.contains(&span_key) { + continue; + } + processed_spans.insert(span_key); + + // Determine the token type based on the context + let token_type = + if ast.parties.iter().any(|p| p.name.value == identifier.value) { + TOKEN_PARTY + } else if ast.policies.iter().any(|p| p.name == identifier.value) { + TOKEN_POLICY + } else if ast.types.iter().any(|t| t.name == identifier.value) { + TOKEN_TYPE + } else if ast.assets.iter().any(|a| a.name == identifier.value) { + TOKEN_CLASS + } else { + // Check if it's a transaction or component of a transaction + let mut found_type = None; + + for tx in &ast.txs { + if tx.name == identifier.value { + found_type = Some(TOKEN_TRANSACTION); + break; + } + + if crate::span_contains(&tx.span, offset) { + // Check if it's a parameter, input, output, or reference + for param in &tx.parameters.parameters { + if param.name == identifier.value { + found_type = Some(TOKEN_PARAMETER); + break; + } + } + + if found_type.is_none() { + for input in &tx.inputs { + if input.name == identifier.value { + found_type = Some(TOKEN_INPUT); + break; + } + } + } + + if found_type.is_none() { + for output in &tx.outputs { + if let Some(output_name) = &output.name { + if *output_name == identifier.value { + found_type = Some(TOKEN_OUTPUT); + break; + } + } + } + } + + if found_type.is_none() { + for reference in &tx.references { + if reference.name == identifier.value { + found_type = Some(TOKEN_REFERENCE); + break; + } + } + } + } + + if found_type.is_some() { + break; + } + } + + // If still not found, treat as a variable + found_type.unwrap_or(TOKEN_VARIABLE) + }; + + // Add the token info + token_infos.push(TokenInfo { + range: crate::span_to_lsp_range(rope, &identifier.span), + token_type, + token_modifiers: MOD_DECLARATION, + }); } } - - // If we can't find keyword, just try to find the identifier - if let Some(id_pos) = text.find(identifier) { - let absolute_id_start = start_char + id_pos; - let absolute_id_end = absolute_id_start + identifier.len(); - - return Some(span_to_lsp_range( - rope, - &tx3_lang::ast::Span::new(absolute_id_start, absolute_id_end), - )); - } - - None - }; - - // Parties - for party in &ast.parties { - if let Some(range) = extract_identifier_after_keyword(&party.span, "party", &party.name) - { - token_infos.push(TokenInfo { - range, - token_type: TOKEN_PARTY, - token_modifiers: MOD_DECLARATION, - }); - } - } - - // Policies - for policy in &ast.policies { - if let Some(range) = - extract_identifier_after_keyword(&policy.span, "policy", &policy.name) - { - token_infos.push(TokenInfo { - range, - token_type: TOKEN_POLICY, - token_modifiers: MOD_DECLARATION, - }); - } - } - - // Types - for type_def in &ast.types { - if let Some(range) = - extract_identifier_after_keyword(&type_def.span, "type", &type_def.name) - { - token_infos.push(TokenInfo { - range, - token_type: TOKEN_TYPE, - token_modifiers: MOD_DECLARATION, - }); - } - } - - // Assets - for asset in &ast.assets { - if let Some(range) = extract_identifier_after_keyword(&asset.span, "asset", &asset.name) - { - token_infos.push(TokenInfo { - range, - token_type: TOKEN_CLASS, - token_modifiers: MOD_DECLARATION, - }); - } - } - - // Transactions - for tx in &ast.txs { - if let Some(range) = extract_identifier_after_keyword(&tx.span, "tx", &tx.name) { - token_infos.push(TokenInfo { - range, - token_type: TOKEN_TRANSACTION, - token_modifiers: MOD_DECLARATION, - }); } } diff --git a/src/server.rs b/src/server.rs index 8718c31..22c6e30 100644 --- a/src/server.rs +++ b/src/server.rs @@ -152,7 +152,7 @@ impl LanguageServer for Context { }; for party in &ast.parties { - if party.name == identifier.value { + if party.name.value == identifier.value { return Ok(Some(GotoDefinitionResponse::Scalar(Location { uri: uri.clone(), range: span_to_lsp_range(document.value(), &party.span), @@ -243,7 +243,7 @@ impl LanguageServer for Context { kind: MarkupKind::Markdown, value: format!( "**Party**: `{}`\n\nA party in the transaction. It can be an address for a script or a wallet.", - party.name + party.name.value ), }), range: Some(span_to_lsp_range(document.value(), &party.span)), @@ -383,7 +383,7 @@ impl LanguageServer for Context { let ast = ast.unwrap(); for party in ast.parties { symbols.push(make_symbol( - party.name.clone(), + party.name.value.clone(), "Party".to_string(), SymbolKind::OBJECT, span_to_lsp_range(document.value(), &party.span), diff --git a/src/visitor.rs b/src/visitor.rs index d591394..682b6e5 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -442,9 +442,12 @@ fn visit_record_field<'a>( } fn visit_party_def<'a>( - _party: &'a tx3_lang::ast::PartyDef, - _offset: usize, + party: &'a tx3_lang::ast::PartyDef, + offset: usize, ) -> Option> { + if in_span(&party.span, offset) { + return Some(SymbolAtOffset::Identifier(&party.name)); + } None } From 41e64b196d00639d011763a5d3672667fa82e9bc Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Thu, 19 Jun 2025 13:16:39 -0300 Subject: [PATCH 08/17] feat: update semantic token types and modifiers for improved clarity and organization --- src/lib.rs | 125 ++++++++++++++++++++++++------------------------- src/server.rs | 16 +++---- src/visitor.rs | 7 +-- 3 files changed, 72 insertions(+), 76 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c037518..9989fe1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -165,23 +165,23 @@ impl Context { rope: &Rope, ) -> Vec { // Token type indices based on the legend order - const TOKEN_KEYWORD: u32 = 0; - const TOKEN_TYPE: u32 = 1; - const TOKEN_PARAMETER: u32 = 2; - const TOKEN_VARIABLE: u32 = 3; - const TOKEN_FUNCTION: u32 = 4; - const TOKEN_CLASS: u32 = 5; - const TOKEN_PROPERTY: u32 = 6; - const TOKEN_PARTY: u32 = 7; - const TOKEN_POLICY: u32 = 8; - const TOKEN_TRANSACTION: u32 = 9; - const TOKEN_INPUT: u32 = 10; - const TOKEN_OUTPUT: u32 = 11; - const TOKEN_REFERENCE: u32 = 12; + // const TOKEN_KEYWORD: u32 = 0; + const TOKEN_TYPE: u32 = 0; + const TOKEN_PARAMETER: u32 = 1; + const TOKEN_VARIABLE: u32 = 2; + // const TOKEN_FUNCTION: u32 = 4; + const TOKEN_CLASS: u32 = 3; + // const TOKEN_PROPERTY: u32 = 6; + const TOKEN_PARTY: u32 = 4; + const TOKEN_POLICY: u32 = 5; + const TOKEN_TRANSACTION: u32 = 6; + const TOKEN_INPUT: u32 = 7; + const TOKEN_OUTPUT: u32 = 8; + const TOKEN_REFERENCE: u32 = 9; // Token modifiers const MOD_DECLARATION: u32 = 1 << 0; - const MOD_DEFINITION: u32 = 1 << 1; + // const MOD_DEFINITION: u32 = 1 << 1; #[derive(Debug, Clone)] struct TokenInfo { @@ -209,72 +209,71 @@ impl Context { processed_spans.insert(span_key); // Determine the token type based on the context - let token_type = - if ast.parties.iter().any(|p| p.name.value == identifier.value) { - TOKEN_PARTY - } else if ast.policies.iter().any(|p| p.name == identifier.value) { - TOKEN_POLICY - } else if ast.types.iter().any(|t| t.name == identifier.value) { - TOKEN_TYPE - } else if ast.assets.iter().any(|a| a.name == identifier.value) { - TOKEN_CLASS - } else { - // Check if it's a transaction or component of a transaction - let mut found_type = None; - - for tx in &ast.txs { - if tx.name == identifier.value { - found_type = Some(TOKEN_TRANSACTION); - break; + let token_type = if ast.parties.iter().any(|p| p.name == identifier.value) { + TOKEN_PARTY + } else if ast.policies.iter().any(|p| p.name == identifier.value) { + TOKEN_POLICY + } else if ast.types.iter().any(|t| t.name == identifier.value) { + TOKEN_TYPE + } else if ast.assets.iter().any(|a| a.name == identifier.value) { + TOKEN_CLASS + } else { + // Check if it's a transaction or component of a transaction + let mut found_type = None; + + for tx in &ast.txs { + if tx.name == identifier.value { + found_type = Some(TOKEN_TRANSACTION); + break; + } + + if crate::span_contains(&tx.span, offset) { + // Check if it's a parameter, input, output, or reference + for param in &tx.parameters.parameters { + if param.name == identifier.value { + found_type = Some(TOKEN_PARAMETER); + break; + } } - if crate::span_contains(&tx.span, offset) { - // Check if it's a parameter, input, output, or reference - for param in &tx.parameters.parameters { - if param.name == identifier.value { - found_type = Some(TOKEN_PARAMETER); + if found_type.is_none() { + for input in &tx.inputs { + if input.name == identifier.value { + found_type = Some(TOKEN_INPUT); break; } } + } - if found_type.is_none() { - for input in &tx.inputs { - if input.name == identifier.value { - found_type = Some(TOKEN_INPUT); + if found_type.is_none() { + for output in &tx.outputs { + if let Some(output_name) = &output.name { + if *output_name == identifier.value { + found_type = Some(TOKEN_OUTPUT); break; } } } + } - if found_type.is_none() { - for output in &tx.outputs { - if let Some(output_name) = &output.name { - if *output_name == identifier.value { - found_type = Some(TOKEN_OUTPUT); - break; - } - } - } - } - - if found_type.is_none() { - for reference in &tx.references { - if reference.name == identifier.value { - found_type = Some(TOKEN_REFERENCE); - break; - } + if found_type.is_none() { + for reference in &tx.references { + if reference.name == identifier.value { + found_type = Some(TOKEN_REFERENCE); + break; } } } + } - if found_type.is_some() { - break; - } + if found_type.is_some() { + break; } + } - // If still not found, treat as a variable - found_type.unwrap_or(TOKEN_VARIABLE) - }; + // If still not found, treat as a variable + found_type.unwrap_or(TOKEN_VARIABLE) + }; // Add the token info token_infos.push(TokenInfo { diff --git a/src/server.rs b/src/server.rs index 22c6e30..4400fca 100644 --- a/src/server.rs +++ b/src/server.rs @@ -28,13 +28,14 @@ impl LanguageServer for Context { work_done_progress_options: WorkDoneProgressOptions::default(), legend: SemanticTokensLegend { token_types: vec![ - SemanticTokenType::KEYWORD, + // SemanticTokenType::KEYWORD, SemanticTokenType::TYPE, SemanticTokenType::PARAMETER, SemanticTokenType::VARIABLE, - SemanticTokenType::FUNCTION, + // SemanticTokenType::FUNCTION, SemanticTokenType::CLASS, - SemanticTokenType::PROPERTY, + // SemanticTokenType::PROPERTY, + // Custom token types for tx3 SemanticTokenType::new("party"), SemanticTokenType::new("policy"), @@ -45,7 +46,7 @@ impl LanguageServer for Context { ], token_modifiers: vec![ SemanticTokenModifier::DECLARATION, - SemanticTokenModifier::DEFINITION, + // SemanticTokenModifier::DEFINITION, SemanticTokenModifier::READONLY, SemanticTokenModifier::STATIC, ], @@ -81,7 +82,6 @@ impl LanguageServer for Context { Ok(Some(CompletionResponse::Array(vec![]))) } - // REVIEW async fn semantic_tokens_full( &self, params: SemanticTokensParams, @@ -152,7 +152,7 @@ impl LanguageServer for Context { }; for party in &ast.parties { - if party.name.value == identifier.value { + if party.name == identifier.value { return Ok(Some(GotoDefinitionResponse::Scalar(Location { uri: uri.clone(), range: span_to_lsp_range(document.value(), &party.span), @@ -243,7 +243,7 @@ impl LanguageServer for Context { kind: MarkupKind::Markdown, value: format!( "**Party**: `{}`\n\nA party in the transaction. It can be an address for a script or a wallet.", - party.name.value + party.name ), }), range: Some(span_to_lsp_range(document.value(), &party.span)), @@ -383,7 +383,7 @@ impl LanguageServer for Context { let ast = ast.unwrap(); for party in ast.parties { symbols.push(make_symbol( - party.name.value.clone(), + party.name.clone(), "Party".to_string(), SymbolKind::OBJECT, span_to_lsp_range(document.value(), &party.span), diff --git a/src/visitor.rs b/src/visitor.rs index 682b6e5..d591394 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -442,12 +442,9 @@ fn visit_record_field<'a>( } fn visit_party_def<'a>( - party: &'a tx3_lang::ast::PartyDef, - offset: usize, + _party: &'a tx3_lang::ast::PartyDef, + _offset: usize, ) -> Option> { - if in_span(&party.span, offset) { - return Some(SymbolAtOffset::Identifier(&party.name)); - } None } From eb0ca4f1329745e47c4e19c99de997a06209ee17 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Thu, 19 Jun 2025 13:17:56 -0300 Subject: [PATCH 09/17] refactor: remove commented-out code --- src/lib.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9989fe1..71f8c16 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -193,10 +193,8 @@ impl Context { let mut token_infos: Vec = Vec::new(); let text = rope.to_string(); - // Find all identifiers in the program let mut processed_spans = std::collections::HashSet::new(); - // We need to scan each position to find symbols using the same mechanism as hover and goto for offset in 0..text.len() { if let Some(symbol) = crate::visitor::find_symbol_in_program(ast, offset) { match symbol { @@ -208,7 +206,6 @@ impl Context { } processed_spans.insert(span_key); - // Determine the token type based on the context let token_type = if ast.parties.iter().any(|p| p.name == identifier.value) { TOKEN_PARTY } else if ast.policies.iter().any(|p| p.name == identifier.value) { @@ -228,7 +225,6 @@ impl Context { } if crate::span_contains(&tx.span, offset) { - // Check if it's a parameter, input, output, or reference for param in &tx.parameters.parameters { if param.name == identifier.value { found_type = Some(TOKEN_PARAMETER); @@ -270,12 +266,9 @@ impl Context { break; } } - - // If still not found, treat as a variable found_type.unwrap_or(TOKEN_VARIABLE) }; - // Add the token info token_infos.push(TokenInfo { range: crate::span_to_lsp_range(rope, &identifier.span), token_type, @@ -285,17 +278,13 @@ impl Context { } } } - - // Sort tokens by position token_infos.sort_by(|a, b| match a.range.start.line.cmp(&b.range.start.line) { std::cmp::Ordering::Equal => a.range.start.character.cmp(&b.range.start.character), other => other, }); - // Remove duplicates token_infos.dedup_by(|a, b| a.range.start == b.range.start && a.range.end == b.range.end); - // Convert to semantic tokens with deltas let mut semantic_tokens = Vec::new(); let mut prev_line = 0; let mut prev_start = 0; From 8e73ca8d3bcbe55104330ec9054502be084e651d Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Thu, 19 Jun 2025 13:19:31 -0300 Subject: [PATCH 10/17] refactor: remove unused get_identifier_at_position function --- src/lib.rs | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 71f8c16..0d2e4f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,31 +75,6 @@ pub fn position_to_offset(text: &str, position: Position) -> usize { offset } -// TODO: Find the smallest span at the offset in the AST -pub fn get_identifier_at_position(text: &str, offset: usize) -> Option { - let chars: Vec = text.chars().collect(); - - if offset >= chars.len() { - return None; - } - - let mut start = offset; - while start > 0 && (chars[start - 1].is_alphanumeric() || chars[start - 1] == '_') { - start -= 1; - } - - let mut end = offset; - while end < chars.len() && (chars[end].is_alphanumeric() || chars[end] == '_') { - end += 1; - } - - if start < end { - Some(chars[start..end].iter().collect()) - } else { - None - } -} - pub fn span_contains(span: &tx3_lang::ast::Span, offset: usize) -> bool { offset >= span.start && offset < span.end } From 82aa5687892a16dfe15519d7bfa9c385332461a5 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Wed, 25 Jun 2025 15:34:30 -0300 Subject: [PATCH 11/17] fix: update identifier access to use value field for consistency --- src/lib.rs | 97 +++++++++++++++++++++++++++------------------------ src/server.rs | 31 +++++++++------- 2 files changed, 69 insertions(+), 59 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0d2e4f6..770ae93 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -181,68 +181,73 @@ impl Context { } processed_spans.insert(span_key); - let token_type = if ast.parties.iter().any(|p| p.name == identifier.value) { - TOKEN_PARTY - } else if ast.policies.iter().any(|p| p.name == identifier.value) { - TOKEN_POLICY - } else if ast.types.iter().any(|t| t.name == identifier.value) { - TOKEN_TYPE - } else if ast.assets.iter().any(|a| a.name == identifier.value) { - TOKEN_CLASS - } else { - // Check if it's a transaction or component of a transaction - let mut found_type = None; - - for tx in &ast.txs { - if tx.name == identifier.value { - found_type = Some(TOKEN_TRANSACTION); - break; - } - - if crate::span_contains(&tx.span, offset) { - for param in &tx.parameters.parameters { - if param.name == identifier.value { - found_type = Some(TOKEN_PARAMETER); - break; - } + let token_type = + if ast.parties.iter().any(|p| p.name.value == identifier.value) { + TOKEN_PARTY + } else if ast + .policies + .iter() + .any(|p| p.name.value == identifier.value) + { + TOKEN_POLICY + } else if ast.types.iter().any(|t| t.name.value == identifier.value) { + TOKEN_TYPE + } else if ast.assets.iter().any(|a| a.name.value == identifier.value) { + TOKEN_CLASS + } else { + // Check if it's a transaction or component of a transaction + let mut found_type = None; + + for tx in &ast.txs { + if tx.name.value == identifier.value { + found_type = Some(TOKEN_TRANSACTION); + break; } - if found_type.is_none() { - for input in &tx.inputs { - if input.name == identifier.value { - found_type = Some(TOKEN_INPUT); + if crate::span_contains(&tx.span, offset) { + for param in &tx.parameters.parameters { + if param.name.value == identifier.value { + found_type = Some(TOKEN_PARAMETER); break; } } - } - if found_type.is_none() { - for output in &tx.outputs { - if let Some(output_name) = &output.name { - if *output_name == identifier.value { - found_type = Some(TOKEN_OUTPUT); + if found_type.is_none() { + for input in &tx.inputs { + if input.name == identifier.value { + found_type = Some(TOKEN_INPUT); break; } } } - } - if found_type.is_none() { - for reference in &tx.references { - if reference.name == identifier.value { - found_type = Some(TOKEN_REFERENCE); - break; + if found_type.is_none() { + for output in &tx.outputs { + if let Some(output_name) = &output.name { + if *output_name == identifier.value { + found_type = Some(TOKEN_OUTPUT); + break; + } + } + } + } + + if found_type.is_none() { + for reference in &tx.references { + if reference.name == identifier.value { + found_type = Some(TOKEN_REFERENCE); + break; + } } } } - } - if found_type.is_some() { - break; + if found_type.is_some() { + break; + } } - } - found_type.unwrap_or(TOKEN_VARIABLE) - }; + found_type.unwrap_or(TOKEN_VARIABLE) + }; token_infos.push(TokenInfo { range: crate::span_to_lsp_range(rope, &identifier.span), diff --git a/src/server.rs b/src/server.rs index 4400fca..d1bbd65 100644 --- a/src/server.rs +++ b/src/server.rs @@ -152,7 +152,7 @@ impl LanguageServer for Context { }; for party in &ast.parties { - if party.name == identifier.value { + if party.name.value == identifier.value { return Ok(Some(GotoDefinitionResponse::Scalar(Location { uri: uri.clone(), range: span_to_lsp_range(document.value(), &party.span), @@ -161,7 +161,7 @@ impl LanguageServer for Context { } for policy in &ast.policies { - if policy.name == identifier.value { + if policy.name.value == identifier.value { return Ok(Some(GotoDefinitionResponse::Scalar(Location { uri: uri.clone(), range: span_to_lsp_range(document.value(), &policy.span), @@ -172,7 +172,7 @@ impl LanguageServer for Context { for tx in &ast.txs { if span_contains(&tx.span, offset) { for param in &tx.parameters.parameters { - if param.name == identifier.value { + if param.name.value == identifier.value { return Ok(Some(GotoDefinitionResponse::Scalar(Location { uri: uri.clone(), range: span_to_lsp_range(document.value(), &tx.parameters.span), @@ -243,7 +243,7 @@ impl LanguageServer for Context { kind: MarkupKind::Markdown, value: format!( "**Party**: `{}`\n\nA party in the transaction. It can be an address for a script or a wallet.", - party.name + party.name.value ), }), range: Some(span_to_lsp_range(document.value(), &party.span)), @@ -256,7 +256,10 @@ impl LanguageServer for Context { return Ok(Some(Hover { contents: HoverContents::Markup(MarkupContent { kind: MarkupKind::Markdown, - value: format!("**Policy**: `{}`\n\nA policy definition.", policy.name), + value: format!( + "**Policy**: `{}`\n\nA policy definition.", + policy.name.value + ), }), range: Some(span_to_lsp_range(document.value(), &policy.span)), })); @@ -297,7 +300,7 @@ impl LanguageServer for Context { kind: MarkupKind::Markdown, value: format!( "**Parameter**: `{}`\n\n**Type**: `{:?}`", - param.name, param.r#type + param.name.value, param.r#type ), }), range: Some(span_to_lsp_range(document.value(), &tx.parameters.span)), @@ -306,13 +309,15 @@ impl LanguageServer for Context { } if span_contains(&tx.span, offset) { - let mut hover_text = format!("**Transaction**: `{}`\n\n", tx.name); + let mut hover_text = format!("**Transaction**: `{}`\n\n", tx.name.value); if !tx.parameters.parameters.is_empty() { hover_text.push_str("**Parameters**:\n"); for param in &tx.parameters.parameters { - hover_text - .push_str(&format!("- `{}`: `{:?}`\n", param.name, param.r#type)); + hover_text.push_str(&format!( + "- `{}`: `{:?}`\n", + param.name.value, param.r#type + )); } hover_text.push_str("\n"); } @@ -383,7 +388,7 @@ impl LanguageServer for Context { let ast = ast.unwrap(); for party in ast.parties { symbols.push(make_symbol( - party.name.clone(), + party.name.value.clone(), "Party".to_string(), SymbolKind::OBJECT, span_to_lsp_range(document.value(), &party.span), @@ -393,7 +398,7 @@ impl LanguageServer for Context { for policy in ast.policies { symbols.push(make_symbol( - policy.name.clone(), + policy.name.value.clone(), "Policy".to_string(), SymbolKind::KEY, span_to_lsp_range(document.value(), &policy.span), @@ -405,7 +410,7 @@ impl LanguageServer for Context { let mut children: Vec = Vec::new(); for parameter in tx.parameters.parameters { children.push(make_symbol( - parameter.name.clone(), + parameter.name.value.clone(), format!("Parameter<{:?}>", parameter.r#type), SymbolKind::FIELD, span_to_lsp_range(document.value(), &tx.parameters.span), @@ -434,7 +439,7 @@ impl LanguageServer for Context { } symbols.push(make_symbol( - tx.name.clone(), + tx.name.value.clone(), "Tx".to_string(), SymbolKind::METHOD, span_to_lsp_range(document.value(), &tx.span), From 7ab0ca727d37e1688332b16c7b1b95cf405bf195 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Wed, 25 Jun 2025 16:26:34 -0300 Subject: [PATCH 12/17] feat: enhance hover functionality to support types and assets in semantic tokens --- src/server.rs | 48 ++++++++++++++++++++++++++++++++++++++++-------- src/visitor.rs | 8 +++++--- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/server.rs b/src/server.rs index d1bbd65..52f5f7b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -151,6 +151,8 @@ impl LanguageServer for Context { SymbolAtOffset::Identifier(x) => x, }; + // TODO - add support for types and assets + for party in &ast.parties { if party.name.value == identifier.value { return Ok(Some(GotoDefinitionResponse::Scalar(Location { @@ -238,30 +240,60 @@ impl LanguageServer for Context { for party in &ast.parties { if span_contains(&party.span, offset) { + return Ok(Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: format!( + "**Party**: `{}`\n\nA party in the transaction. It can be an address for a script or a wallet.", + party.name.value + ), + }), + range: Some(span_to_lsp_range(document.value(), &party.span)), + })); + } + } + + for policy in &ast.policies { + if span_contains(&policy.span, offset) { return Ok(Some(Hover { contents: HoverContents::Markup(MarkupContent { kind: MarkupKind::Markdown, value: format!( - "**Party**: `{}`\n\nA party in the transaction. It can be an address for a script or a wallet.", - party.name.value + "**Policy**: `{}`\n\nA policy definition.", + policy.name.value ), }), - range: Some(span_to_lsp_range(document.value(), &party.span)), + range: Some(span_to_lsp_range(document.value(), &policy.span)), })); } } - for policy in &ast.policies { - if span_contains(&policy.span, offset) { + for type_def in &ast.types { + if span_contains(&type_def.span, offset) { return Ok(Some(Hover { contents: HoverContents::Markup(MarkupContent { kind: MarkupKind::Markdown, value: format!( - "**Policy**: `{}`\n\nA policy definition.", - policy.name.value + "**Type**: `{}`\n\nA type definition.", + type_def.name.value ), }), - range: Some(span_to_lsp_range(document.value(), &policy.span)), + range: Some(span_to_lsp_range(document.value(), &type_def.span)), + })); + } + } + + for asset in &ast.assets { + if span_contains(&asset.span, offset) { + return Ok(Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: format!( + "**Asset**: `{}`\n\nAn asset definition.", + asset.name.value + ), + }), + range: Some(span_to_lsp_range(document.value(), &asset.span)), })); } } diff --git a/src/visitor.rs b/src/visitor.rs index d591394..db42935 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -1,7 +1,6 @@ #[derive(Debug)] pub enum SymbolAtOffset<'a> { Identifier(&'a tx3_lang::ast::Identifier), - // add more symbol variants if needed } pub fn find_symbol_in_program<'a>( @@ -442,9 +441,12 @@ fn visit_record_field<'a>( } fn visit_party_def<'a>( - _party: &'a tx3_lang::ast::PartyDef, - _offset: usize, + party: &'a tx3_lang::ast::PartyDef, + offset: usize, ) -> Option> { + if in_span(&party.span, offset) { + return Some(SymbolAtOffset::Identifier(&party.name)); + } None } From 36c4063c7b44e48cd073f523b8d96b7b4cea92b8 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Thu, 26 Jun 2025 16:03:12 -0300 Subject: [PATCH 13/17] refactor: update semantic token types and improve identifier handling in visitor --- src/lib.rs | 41 ++++------------------------------------- src/server.rs | 12 +++--------- src/visitor.rs | 19 ++++++++++++++++++- 3 files changed, 25 insertions(+), 47 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 770ae93..da27279 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -140,19 +140,15 @@ impl Context { rope: &Rope, ) -> Vec { // Token type indices based on the legend order - // const TOKEN_KEYWORD: u32 = 0; const TOKEN_TYPE: u32 = 0; const TOKEN_PARAMETER: u32 = 1; const TOKEN_VARIABLE: u32 = 2; - // const TOKEN_FUNCTION: u32 = 4; const TOKEN_CLASS: u32 = 3; - // const TOKEN_PROPERTY: u32 = 6; const TOKEN_PARTY: u32 = 4; const TOKEN_POLICY: u32 = 5; - const TOKEN_TRANSACTION: u32 = 6; - const TOKEN_INPUT: u32 = 7; - const TOKEN_OUTPUT: u32 = 8; - const TOKEN_REFERENCE: u32 = 9; + const TOKEN_FUNCTION: u32 = 6; + // const TOKEN_KEYWORD: u32 = 7; + // const TOKEN_PROPERTY: u32 = 8; // Token modifiers const MOD_DECLARATION: u32 = 1 << 0; @@ -200,7 +196,7 @@ impl Context { for tx in &ast.txs { if tx.name.value == identifier.value { - found_type = Some(TOKEN_TRANSACTION); + found_type = Some(TOKEN_FUNCTION); break; } @@ -211,35 +207,6 @@ impl Context { break; } } - - if found_type.is_none() { - for input in &tx.inputs { - if input.name == identifier.value { - found_type = Some(TOKEN_INPUT); - break; - } - } - } - - if found_type.is_none() { - for output in &tx.outputs { - if let Some(output_name) = &output.name { - if *output_name == identifier.value { - found_type = Some(TOKEN_OUTPUT); - break; - } - } - } - } - - if found_type.is_none() { - for reference in &tx.references { - if reference.name == identifier.value { - found_type = Some(TOKEN_REFERENCE); - break; - } - } - } } if found_type.is_some() { diff --git a/src/server.rs b/src/server.rs index 52f5f7b..9248f48 100644 --- a/src/server.rs +++ b/src/server.rs @@ -28,21 +28,15 @@ impl LanguageServer for Context { work_done_progress_options: WorkDoneProgressOptions::default(), legend: SemanticTokensLegend { token_types: vec![ - // SemanticTokenType::KEYWORD, SemanticTokenType::TYPE, SemanticTokenType::PARAMETER, SemanticTokenType::VARIABLE, - // SemanticTokenType::FUNCTION, SemanticTokenType::CLASS, - // SemanticTokenType::PROPERTY, - - // Custom token types for tx3 SemanticTokenType::new("party"), SemanticTokenType::new("policy"), - SemanticTokenType::new("transaction"), - SemanticTokenType::new("input"), - SemanticTokenType::new("output"), - SemanticTokenType::new("reference"), + SemanticTokenType::FUNCTION, + // SemanticTokenType::KEYWORD, + // SemanticTokenType::PROPERTY, ], token_modifiers: vec![ SemanticTokenModifier::DECLARATION, diff --git a/src/visitor.rs b/src/visitor.rs index db42935..6b9a1a1 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -36,6 +36,9 @@ pub fn find_symbol_in_program<'a>( } fn visit_tx_def<'a>(tx: &'a tx3_lang::ast::TxDef, offset: usize) -> Option> { + if in_span(&tx.name.span, offset) { + return Some(SymbolAtOffset::Identifier(&tx.name)); + } if let Some(sym) = visit_parameter_list(&tx.parameters, offset) { return Some(sym); } @@ -97,6 +100,9 @@ fn visit_parameter_list<'a>( offset: usize, ) -> Option> { for param in ¶ms.parameters { + if in_span(¶m.name.span, offset) { + return Some(SymbolAtOffset::Identifier(¶m.name)); + } if let Some(sym) = visit_type(¶m.r#type, offset) { return Some(sym); } @@ -105,6 +111,7 @@ fn visit_parameter_list<'a>( } fn visit_type<'a>(ty: &'a tx3_lang::ast::Type, offset: usize) -> Option> { + // TODO - complete for all types match ty { tx3_lang::ast::Type::Custom(id) => visit_identifier(id, offset), tx3_lang::ast::Type::List(inner) => visit_type(inner, offset), @@ -413,6 +420,9 @@ fn visit_asset_def<'a>( } fn visit_type_def<'a>(ty: &'a tx3_lang::ast::TypeDef, offset: usize) -> Option> { + if in_span(&ty.span, offset) { + return Some(SymbolAtOffset::Identifier(&ty.name)); + } for case in &ty.cases { if let Some(sym) = visit_variant_case(case, offset) { return Some(sym); @@ -437,6 +447,9 @@ fn visit_record_field<'a>( field: &'a tx3_lang::ast::RecordField, offset: usize, ) -> Option> { + if in_span(&field.name.span, offset) { + return Some(SymbolAtOffset::Identifier(&field.name)); + } visit_type(&field.r#type, offset) } @@ -462,7 +475,11 @@ fn visit_policy_def<'a>( } } } - tx3_lang::ast::PolicyValue::Assign(_) => {} + tx3_lang::ast::PolicyValue::Assign(_) => { + if in_span(&policy.span, offset) { + return Some(SymbolAtOffset::Identifier(&policy.name)); + } + } } None } From ad998af66b92bdf9f85fa3cacfcca1e2ad71b3fc Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Fri, 27 Jun 2025 15:31:59 -0300 Subject: [PATCH 14/17] feat: add support for type field references in semantic token collection --- src/lib.rs | 100 ++++++++++++++++++++++++++++++++----------------- src/server.rs | 11 ++++-- src/visitor.rs | 12 ++++-- 3 files changed, 82 insertions(+), 41 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index da27279..8fc8f91 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -134,6 +134,24 @@ pub struct Context { } impl Context { + fn is_type_field_reference( + ast: &tx3_lang::ast::Program, + identifier: &str, + offset: usize, + ) -> bool { + for type_def in &ast.types { + if crate::span_contains(&type_def.span, offset) { + for case in &type_def.cases { + for field in &case.fields { + if identifier == field.r#type.clone().to_str() { + return true; + } + } + } + } + } + false + } fn collect_semantic_tokens( &self, ast: &tx3_lang::ast::Program, @@ -152,7 +170,7 @@ impl Context { // Token modifiers const MOD_DECLARATION: u32 = 1 << 0; - // const MOD_DEFINITION: u32 = 1 << 1; + const MOD_DEFINITION: u32 = 1 << 1; #[derive(Debug, Clone)] struct TokenInfo { @@ -177,49 +195,61 @@ impl Context { } processed_spans.insert(span_key); - let token_type = - if ast.parties.iter().any(|p| p.name.value == identifier.value) { - TOKEN_PARTY - } else if ast - .policies - .iter() - .any(|p| p.name.value == identifier.value) - { - TOKEN_POLICY - } else if ast.types.iter().any(|t| t.name.value == identifier.value) { - TOKEN_TYPE - } else if ast.assets.iter().any(|a| a.name.value == identifier.value) { - TOKEN_CLASS - } else { - // Check if it's a transaction or component of a transaction - let mut found_type = None; - - for tx in &ast.txs { - if tx.name.value == identifier.value { - found_type = Some(TOKEN_FUNCTION); - break; - } + let token_type = if ast + .parties + .iter() + .any(|p| p.name.value == identifier.value) + { + TOKEN_PARTY + } else if ast + .policies + .iter() + .any(|p| p.name.value == identifier.value) + { + TOKEN_POLICY + } else if ast.types.iter().any(|t| t.name.value == identifier.value) { + TOKEN_TYPE + } else if Context::is_type_field_reference(ast, &identifier.value, offset) { + TOKEN_TYPE + } else if ast.assets.iter().any(|a| a.name.value == identifier.value) { + TOKEN_CLASS + } else { + // Check if it's a transaction or component of a transaction + let mut found_type = None; + + for tx in &ast.txs { + if tx.name.value == identifier.value { + found_type = Some(TOKEN_FUNCTION); + break; + } - if crate::span_contains(&tx.span, offset) { - for param in &tx.parameters.parameters { - if param.name.value == identifier.value { - found_type = Some(TOKEN_PARAMETER); - break; - } + if crate::span_contains(&tx.span, offset) { + for param in &tx.parameters.parameters { + if param.name.value == identifier.value { + found_type = Some(TOKEN_PARAMETER); + break; } } + } - if found_type.is_some() { - break; - } + if found_type.is_some() { + break; } - found_type.unwrap_or(TOKEN_VARIABLE) - }; + } + found_type.unwrap_or(TOKEN_VARIABLE) + }; token_infos.push(TokenInfo { range: crate::span_to_lsp_range(rope, &identifier.span), token_type, - token_modifiers: MOD_DECLARATION, + token_modifiers: MOD_DECLARATION | MOD_DEFINITION, + }); + } + visitor::SymbolAtOffset::TypeIdentifier(x) => { + token_infos.push(TokenInfo { + range: crate::span_to_lsp_range(rope, &x.span), + token_type: TOKEN_TYPE, + token_modifiers: MOD_DECLARATION | MOD_DEFINITION, }); } } diff --git a/src/server.rs b/src/server.rs index 9248f48..2464962 100644 --- a/src/server.rs +++ b/src/server.rs @@ -40,7 +40,7 @@ impl LanguageServer for Context { ], token_modifiers: vec![ SemanticTokenModifier::DECLARATION, - // SemanticTokenModifier::DEFINITION, + SemanticTokenModifier::DEFINITION, SemanticTokenModifier::READONLY, SemanticTokenModifier::STATIC, ], @@ -143,10 +143,15 @@ impl LanguageServer for Context { if let Some(symbol) = find_symbol_in_program(&ast, offset) { let identifier = match symbol { SymbolAtOffset::Identifier(x) => x, + SymbolAtOffset::TypeIdentifier(type_record) => { + // TODO - look only in type definitions + return Ok(Some(GotoDefinitionResponse::Scalar(Location { + uri: uri.clone(), + range: span_to_lsp_range(document.value(), &type_record.span), + }))); + } }; - // TODO - add support for types and assets - for party in &ast.parties { if party.name.value == identifier.value { return Ok(Some(GotoDefinitionResponse::Scalar(Location { diff --git a/src/visitor.rs b/src/visitor.rs index 6b9a1a1..64237a7 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -1,6 +1,7 @@ #[derive(Debug)] pub enum SymbolAtOffset<'a> { Identifier(&'a tx3_lang::ast::Identifier), + TypeIdentifier(&'a tx3_lang::ast::TypeRecord), } pub fn find_symbol_in_program<'a>( @@ -110,9 +111,9 @@ fn visit_parameter_list<'a>( None } -fn visit_type<'a>(ty: &'a tx3_lang::ast::Type, offset: usize) -> Option> { +fn visit_type<'a>(ty: &'a tx3_lang::ast::TypeRecord, offset: usize) -> Option> { // TODO - complete for all types - match ty { + match &ty.r#type { tx3_lang::ast::Type::Custom(id) => visit_identifier(id, offset), tx3_lang::ast::Type::List(inner) => visit_type(inner, offset), _ => None, @@ -420,10 +421,15 @@ fn visit_asset_def<'a>( } fn visit_type_def<'a>(ty: &'a tx3_lang::ast::TypeDef, offset: usize) -> Option> { - if in_span(&ty.span, offset) { + if in_span(&ty.name.span, offset) { return Some(SymbolAtOffset::Identifier(&ty.name)); } for case in &ty.cases { + for field in &case.fields { + if in_span(&field.r#type.span, offset) { + return Some(SymbolAtOffset::TypeIdentifier(&field.r#type)); + } + } if let Some(sym) = visit_variant_case(case, offset) { return Some(sym); } From 3743039593d26a83bdedfbf3391203b450cb841f Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Fri, 27 Jun 2025 15:34:55 -0300 Subject: [PATCH 15/17] refactor: remove comments --- src/lib.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8fc8f91..b5479a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -157,7 +157,6 @@ impl Context { ast: &tx3_lang::ast::Program, rope: &Rope, ) -> Vec { - // Token type indices based on the legend order const TOKEN_TYPE: u32 = 0; const TOKEN_PARAMETER: u32 = 1; const TOKEN_VARIABLE: u32 = 2; @@ -168,7 +167,6 @@ impl Context { // const TOKEN_KEYWORD: u32 = 7; // const TOKEN_PROPERTY: u32 = 8; - // Token modifiers const MOD_DECLARATION: u32 = 1 << 0; const MOD_DEFINITION: u32 = 1 << 1; @@ -214,7 +212,6 @@ impl Context { } else if ast.assets.iter().any(|a| a.name.value == identifier.value) { TOKEN_CLASS } else { - // Check if it's a transaction or component of a transaction let mut found_type = None; for tx in &ast.txs { From bffc4657c74a88bc96fe97e465119b95b56dfa89 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Fri, 27 Jun 2025 16:09:52 -0300 Subject: [PATCH 16/17] refactor: replace asset expression handling with data expression in input and output blocks --- src/visitor.rs | 63 ++++---------------------------------------------- 1 file changed, 5 insertions(+), 58 deletions(-) diff --git a/src/visitor.rs b/src/visitor.rs index 64237a7..e9d2325 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -150,7 +150,7 @@ fn visit_input_block_field<'a>( match field { tx3_lang::ast::InputBlockField::From(addr) => visit_address_expr(addr, offset), tx3_lang::ast::InputBlockField::DatumIs(ty) => visit_type(ty, offset), - tx3_lang::ast::InputBlockField::MinAmount(expr) => visit_asset_expr(expr, offset), + tx3_lang::ast::InputBlockField::MinAmount(expr) => visit_data_expr(expr, offset), tx3_lang::ast::InputBlockField::Redeemer(expr) => visit_data_expr(expr, offset), tx3_lang::ast::InputBlockField::Ref(expr) => visit_data_expr(expr, offset), } @@ -174,42 +174,11 @@ fn visit_output_block_field<'a>( ) -> Option> { match field { tx3_lang::ast::OutputBlockField::To(addr) => visit_address_expr(addr, offset), - tx3_lang::ast::OutputBlockField::Amount(expr) => visit_asset_expr(expr, offset), + tx3_lang::ast::OutputBlockField::Amount(expr) => visit_data_expr(expr, offset), tx3_lang::ast::OutputBlockField::Datum(expr) => visit_data_expr(expr, offset), } } -fn visit_asset_expr<'a>( - expr: &'a tx3_lang::ast::AssetExpr, - offset: usize, -) -> Option> { - match expr { - tx3_lang::ast::AssetExpr::Identifier(id) => visit_identifier(id, offset), - tx3_lang::ast::AssetExpr::StaticConstructor(constr) => { - if let Some(sym) = visit_identifier(&constr.r#type, offset) { - return Some(sym); - } - visit_data_expr(&constr.amount, offset) - } - tx3_lang::ast::AssetExpr::AnyConstructor(constr) => { - if let Some(sym) = visit_data_expr(&constr.policy, offset) { - return Some(sym); - } - if let Some(sym) = visit_data_expr(&constr.asset_name, offset) { - return Some(sym); - } - visit_data_expr(&constr.amount, offset) - } - tx3_lang::ast::AssetExpr::BinaryOp(binop) => { - if let Some(sym) = visit_asset_expr(&binop.left, offset) { - return Some(sym); - } - visit_asset_expr(&binop.right, offset) - } - tx3_lang::ast::AssetExpr::PropertyAccess(pa) => visit_property_access(pa, offset), - } -} - fn visit_data_expr<'a>( expr: &'a tx3_lang::ast::DataExpr, offset: usize, @@ -225,13 +194,6 @@ fn visit_data_expr<'a>( } None } - tx3_lang::ast::DataExpr::PropertyAccess(pa) => visit_property_access(pa, offset), - tx3_lang::ast::DataExpr::BinaryOp(binop) => { - if let Some(sym) = visit_data_expr(&binop.left, offset) { - return Some(sym); - } - visit_data_expr(&binop.right, offset) - } _ => None, } } @@ -274,21 +236,6 @@ fn visit_record_constructor_field<'a>( visit_data_expr(&field.value, offset) } -fn visit_property_access<'a>( - pa: &'a tx3_lang::ast::PropertyAccess, - offset: usize, -) -> Option> { - if let Some(sym) = visit_identifier(&pa.object, offset) { - return Some(sym); - } - for id in &pa.path { - if let Some(sym) = visit_identifier(id, offset) { - return Some(sym); - } - } - None -} - fn visit_reference_block<'a>( rb: &'a tx3_lang::ast::ReferenceBlock, offset: usize, @@ -315,7 +262,7 @@ fn visit_collateral_block<'a>( } } tx3_lang::ast::CollateralBlockField::MinAmount(expr) => { - if let Some(sym) = visit_asset_expr(expr, offset) { + if let Some(sym) = visit_data_expr(expr, offset) { return Some(sym); } } @@ -365,7 +312,7 @@ fn visit_burn_block<'a>( for field in &bb.fields { match field { tx3_lang::ast::MintBlockField::Amount(expr) => { - if let Some(sym) = visit_asset_expr(expr, offset) { + if let Some(sym) = visit_data_expr(expr, offset) { return Some(sym); } } @@ -393,7 +340,7 @@ fn visit_mint_block<'a>( for field in &mb.fields { match field { tx3_lang::ast::MintBlockField::Amount(expr) => { - if let Some(sym) = visit_asset_expr(expr, offset) { + if let Some(sym) = visit_data_expr(expr, offset) { return Some(sym); } } From ee46cf6190253a7af4ddb3bac93955e378e933e9 Mon Sep 17 00:00:00 2001 From: Santiago Carmuega Date: Fri, 4 Jul 2025 08:45:58 -0300 Subject: [PATCH 17/17] use tx3 v0.7 --- Cargo.lock | 31 +++++++++++++++++++++++++++---- Cargo.toml | 2 +- src/lib.rs | 14 ++++++++------ src/server.rs | 11 ++++------- src/visitor.rs | 14 ++++++++------ 5 files changed, 48 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d31cbe..5726623 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,11 +71,22 @@ dependencies = [ [[package]] name = "bincode" -version = "1.3.3" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" dependencies = [ + "bincode_derive", "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", ] [[package]] @@ -1117,9 +1128,9 @@ dependencies = [ [[package]] name = "tx3-lang" -version = "0.4.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b33d3688e704cfb6401b2272fbaee645f2e51fb215f995288bdb5dfd9b1ee3" +checksum = "c0a45723cea8deff15587048a1b4a2fa6d4fe9ac5c0e66ab2e9a4d22b2eb6bbd" dependencies = [ "bincode", "hex", @@ -1184,6 +1195,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "url" version = "2.5.4" @@ -1214,6 +1231,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 57d66f1..f541e23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ tower-lsp = "0.20.0" tower = { version = "0.4", features = ["util"] } dashmap = "6.1.0" ropey = "1.6.1" -tx3-lang = "0.4.1" +tx3-lang = "0.7.0" pest = "2.7.15" miette = "7.5.0" serde_json = "1.0.140" diff --git a/src/lib.rs b/src/lib.rs index b5479a5..dcc5571 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -143,7 +143,7 @@ impl Context { if crate::span_contains(&type_def.span, offset) { for case in &type_def.cases { for field in &case.fields { - if identifier == field.r#type.clone().to_str() { + if identifier == field.r#type.to_string() { return true; } } @@ -243,11 +243,13 @@ impl Context { }); } visitor::SymbolAtOffset::TypeIdentifier(x) => { - token_infos.push(TokenInfo { - range: crate::span_to_lsp_range(rope, &x.span), - token_type: TOKEN_TYPE, - token_modifiers: MOD_DECLARATION | MOD_DEFINITION, - }); + // TODO: wait for the introduction of `TypeAnnotation` in AST + + // token_infos.push(TokenInfo { + // range: crate::span_to_lsp_range(rope, &x.span), + // token_type: TOKEN_TYPE, + // token_modifiers: MOD_DECLARATION | MOD_DEFINITION, + // }); } } } diff --git a/src/server.rs b/src/server.rs index 2464962..9ba4d4f 100644 --- a/src/server.rs +++ b/src/server.rs @@ -143,13 +143,10 @@ impl LanguageServer for Context { if let Some(symbol) = find_symbol_in_program(&ast, offset) { let identifier = match symbol { SymbolAtOffset::Identifier(x) => x, - SymbolAtOffset::TypeIdentifier(type_record) => { - // TODO - look only in type definitions - return Ok(Some(GotoDefinitionResponse::Scalar(Location { - uri: uri.clone(), - range: span_to_lsp_range(document.value(), &type_record.span), - }))); - } + SymbolAtOffset::TypeIdentifier(ty) => match ty { + tx3_lang::ast::Type::Custom(x) => x, + _ => return Ok(None), + }, }; for party in &ast.parties { diff --git a/src/visitor.rs b/src/visitor.rs index e9d2325..e9b3bfa 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -1,7 +1,7 @@ #[derive(Debug)] pub enum SymbolAtOffset<'a> { Identifier(&'a tx3_lang::ast::Identifier), - TypeIdentifier(&'a tx3_lang::ast::TypeRecord), + TypeIdentifier(&'a tx3_lang::ast::Type), } pub fn find_symbol_in_program<'a>( @@ -111,9 +111,9 @@ fn visit_parameter_list<'a>( None } -fn visit_type<'a>(ty: &'a tx3_lang::ast::TypeRecord, offset: usize) -> Option> { +fn visit_type<'a>(ty: &'a tx3_lang::ast::Type, offset: usize) -> Option> { // TODO - complete for all types - match &ty.r#type { + match &ty { tx3_lang::ast::Type::Custom(id) => visit_identifier(id, offset), tx3_lang::ast::Type::List(inner) => visit_type(inner, offset), _ => None, @@ -373,9 +373,11 @@ fn visit_type_def<'a>(ty: &'a tx3_lang::ast::TypeDef, offset: usize) -> Option