From f5fcbbacb64cef8dd80720cb8e6955bcb38e839c Mon Sep 17 00:00:00 2001 From: Selimaj Dev Date: Sun, 24 May 2026 15:11:24 +0200 Subject: [PATCH 1/8] did change and close --- crates/analyzer/src/lib.rs | 67 +++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index 28e548d..cf93b63 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -93,7 +93,7 @@ impl LanguageServer for Backend { res.capabilities.text_document_sync = Some(TextDocumentSyncCapability::Options( TextDocumentSyncOptions { open_close: Some(true), - change: Some(TextDocumentSyncKind::INCREMENTAL), + change: Some(TextDocumentSyncKind::FULL), will_save: Some(false), will_save_wait_until: Some(false), save: Some(SaveOptions::default().into()), @@ -347,6 +347,71 @@ impl LanguageServer for Backend { }) } + async fn did_change(&self, mut params: DidChangeTextDocumentParams) { + self.client + .log_message(MessageType::INFO, "MIST-LSP: Processing did_change event") + .await; + + let rust_path = from_mist_to_rust(params.text_document.uri.to_file_path().unwrap()); + let rust_uri = Url::from_file_path(&rust_path).unwrap(); + + if let Some(change) = params.content_changes.first_mut() { + match transpiler::transpile_text(&change.text) { + Ok(transpiled_text) => { + change.text = transpiled_text; + + self.mapping + .lock() + .await + .insert(rust_path, rev_mapper::get_mapping(&change.text)); + } + Err(e) => { + self.client + .log_message( + MessageType::WARNING, + format!( + "MIST-LSP: Syntax invalid during change. Sync stopped: {:?}", + e + ), + ) + .await; + return; + } + } + } + + // 3. Re-route URI path to mirror directory + params.text_document.uri = rust_uri; + + // 4. Notify downstream rust-analyzer + if let Ok(mut ra) = self.rust_analyzer.try_lock() { + let _ = ra + .notify(notification::DidChangeTextDocument::METHOD, params) + .await; + } + } + + async fn did_close(&self, mut params: DidCloseTextDocumentParams) { + self.client + .log_message(MessageType::INFO, "MIST-LSP: Processing did_close event") + .await; + + let rust_path = from_mist_to_rust(params.text_document.uri.to_file_path().unwrap()); + + // 1. Evict the mapping from memory to prevent leaks + self.mapping.lock().await.remove(&rust_path); + + // 2. Re-route URI path to mirror directory + params.text_document.uri = Url::from_file_path(&rust_path).unwrap(); + + // 3. Notify downstream rust-analyzer + if let Ok(mut ra) = self.rust_analyzer.try_lock() { + let _ = ra + .notify(notification::DidCloseTextDocument::METHOD, params) + .await; + } + } + async fn completion(&self, _params: CompletionParams) -> Result> { self.client .log_message(MessageType::INFO, "getting completion") From 354965b425b21b5fca6ae7766c4b460c0faae5d5 Mon Sep 17 00:00:00 2001 From: Selimaj Dev Date: Sun, 24 May 2026 15:45:01 +0200 Subject: [PATCH 2/8] Completion --- crates/analyzer/src/lib.rs | 113 +++++++++++++++++++------------------ 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index cf93b63..75f9a6f 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -101,8 +101,11 @@ impl LanguageServer for Backend { )); res.capabilities.completion_provider = Some(CompletionOptions { - resolve_provider: None, + resolve_provider: Some(true), trigger_characters: Some(vec![".".to_string()]), + completion_item: Some(CompletionOptionsCompletionItem { + label_details_support: Some(true), + }), ..Default::default() }); @@ -132,6 +135,8 @@ impl LanguageServer for Backend { .initialize(root) .await .expect("Failed to initialize rust analyzer"); + + eprintln!("Ready to use"); } }); @@ -139,16 +144,16 @@ impl LanguageServer for Backend { } async fn initialized(&self, _: InitializedParams) { - self.client - .log_message(MessageType::INFO, "server initialized!") - .await; - self.rust_analyzer .lock() .await .initialized() .await .expect("Failed to initialize rust analyzer"); + + self.client + .log_message(MessageType::INFO, "server initialized!") + .await; } async fn shutdown(&self) -> Result<()> { @@ -412,60 +417,58 @@ impl LanguageServer for Backend { } } - async fn completion(&self, _params: CompletionParams) -> Result> { + async fn completion(&self, mut params: CompletionParams) -> Result> { self.client - .log_message(MessageType::INFO, "getting completion") + .log_message(MessageType::INFO, "COMPLETEING!") .await; - Ok(Some(CompletionResponse::Array(vec![ - CompletionItem::new_simple("new".to_string(), "The new keyword".to_string()), - ]))) + let file_path = params + .text_document_position + .text_document + .uri + .to_file_path() + .unwrap(); + + let source = match fs::read_to_string(&file_path) { + Ok(src) => src, + Err(_) => return Ok(None), + }; + + let inject = "__mist_23"; + let injected_source = insert_at_position( + &source, + params.text_document_position.position.line as usize + 1, + params.text_document_position.position.character as usize, + &inject, + ); + + let output = match transpiler::transpile_text(&injected_source) { + Ok(out) => out, + Err(_) => return Ok(None), + }; + + let (line, character) = match find_row_col(&output, inject) { + Some(coords) => coords, + None => return Ok(None), + }; + + let uri = + Url::from_file_path(from_mist_to_rust(file_path)).expect("failed to generate rs url"); + + params.text_document_position.text_document.uri = uri; + params.text_document_position.position.line = line as u32; + params.text_document_position.position.character = character as u32; + + let rs_res = self + .rust_analyzer + .lock() + .await + .request::(params) + .await + .expect("Failed to send to rust"); + + Ok(rs_res) } - - // async fn completion(&self, mut params: CompletionParams) -> Result> { - // self.client - // .log_message(MessageType::INFO, "COMPLETEING!") - // .await; - - // let file_path = params - // .text_document_position - // .text_document - // .uri - // .to_file_path() - // .unwrap(); - - // let mut source = fs::read_to_string(&file_path).expect("Failed to read source"); - - // let inject = "__mist_23"; - - // source = insert_at_position( - // &source, - // params.text_document_position.position.line as usize + 1, - // params.text_document_position.position.character as usize, - // &inject, - // ); - - // let output = transpiler::transpile_text(&source).expect("Failed to transpile"); - - // let (line, character) = find_row_col(&output, inject).unwrap(); - - // let uri = - // Url::from_file_path(from_mist_to_rust(file_path)).expect("failed to generate rs url"); - - // params.text_document_position.text_document.uri = uri; - // params.text_document_position.position.line = line as u32; - // params.text_document_position.position.character = character as u32; - - // let rs_res = self - // .rust_analyzer - // .lock() - // .await - // .request::(params) - // .await - // .expect("Failed to send to rust"); - - // Ok(rs_res) - // } } #[tokio::main] From 5a6adbc5e4c88bac339512be5e45964c7d255a91 Mon Sep 17 00:00:00 2001 From: Selimaj Dev Date: Sun, 24 May 2026 16:19:51 +0200 Subject: [PATCH 3/8] updated capabilities --- crates/analyzer/src/lib.rs | 14 +++++++++++--- crates/analyzer/src/rust_analyzer.rs | 6 ++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index 75f9a6f..be45dbd 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -102,11 +102,19 @@ impl LanguageServer for Backend { res.capabilities.completion_provider = Some(CompletionOptions { resolve_provider: Some(true), - trigger_characters: Some(vec![".".to_string()]), + trigger_characters: Some(vec![ + ":".to_owned(), + ".".to_owned(), + "'".to_owned(), + "(".to_owned(), + ]), + all_commit_characters: None, completion_item: Some(CompletionOptionsCompletionItem { - label_details_support: Some(true), + label_details_support: None, }), - ..Default::default() + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: None, + }, }); res.capabilities.definition_provider = Some(OneOf::Left(true)); diff --git a/crates/analyzer/src/rust_analyzer.rs b/crates/analyzer/src/rust_analyzer.rs index e2cc377..b7e584d 100644 --- a/crates/analyzer/src/rust_analyzer.rs +++ b/crates/analyzer/src/rust_analyzer.rs @@ -175,13 +175,15 @@ impl RustAnalyzer { let value = rx.await?; - let envelope: JsonRpcResponse = serde_json::from_value(value)?; + let envelope: JsonRpcResponse = serde_json::from_value(value.clone())?; if let Some(err) = envelope.error { return Err(format!("LSP Error ({}): {}", err.code, err.message).into()); } - envelope.result.ok_or_else(|| "missing result".into()) + envelope + .result + .ok_or_else(|| format!("missing result: {:?}", value).into()) } pub async fn notify(&mut self, method: &str, req: T) -> std::io::Result<()> { From fbf60944a877aa39139654b45a62d99487719f1d Mon Sep 17 00:00:00 2001 From: Selimaj Dev Date: Sun, 24 May 2026 16:24:22 +0200 Subject: [PATCH 4/8] using ropey --- Cargo.lock | 17 ++++++++++++ crates/analyzer/Cargo.toml | 1 + crates/analyzer/src/lib.rs | 55 +++++++++++++++++++++----------------- 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c393756..70d4730 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,6 +448,7 @@ dependencies = [ "dashmap 6.1.0", "mist-codegen", "mist-parser", + "ropey", "serde", "serde_json", "tokio", @@ -623,6 +624,16 @@ dependencies = [ "bitflags 2.11.1", ] +[[package]] +name = "ropey" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93411e420bcd1a75ddd1dc3caf18c23155eda2c090631a85af21ba19e97093b5" +dependencies = [ + "smallvec", + "str_indices", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -742,6 +753,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "str_indices" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6" + [[package]] name = "syn" version = "2.0.117" diff --git a/crates/analyzer/Cargo.toml b/crates/analyzer/Cargo.toml index 7a9cc43..d8eda02 100644 --- a/crates/analyzer/Cargo.toml +++ b/crates/analyzer/Cargo.toml @@ -19,3 +19,4 @@ cargo_metadata = "0.23.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +ropey = "1" diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index be45dbd..7397b4c 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -8,6 +8,7 @@ use std::path::{Component, PathBuf}; use std::sync::Arc; use mist_parser::rev_mapper; +use ropey::Rope; use tokio::sync::Mutex; use tower_lsp::jsonrpc::Result; use tower_lsp::lsp_types::notification::Notification; @@ -539,44 +540,50 @@ pub fn from_rust_to_mist(mut path: PathBuf) -> PathBuf { } fn insert_at_position(s: &str, line: usize, col: usize, insert: &str) -> String { - let mut lines: Vec = s.lines().map(|l| l.to_string()).collect(); + let mut rope = Rope::from_str(s); - // Convert to 0-index + // Convert to 0-indexed let line_idx = line.saturating_sub(1); let col_idx = col.saturating_sub(1); - // Ensure line exists (optional behavior: extend with empty lines) - if line_idx >= lines.len() { - lines.resize(line_idx + 1, String::new()); - } + // Clamp line + let line_idx = line_idx.min(rope.len_lines().saturating_sub(1)); - let target = &mut lines[line_idx]; + // Get start char index of the line + let line_start = rope.line_to_char(line_idx); - // Clamp column to valid char boundary - let char_idx = target - .char_indices() - .nth(col_idx) - .map(|(i, _)| i) - .unwrap_or(target.len()); // if past end, append + // Get line length in chars + let line = rope.line(line_idx); + let line_len = line.len_chars(); - target.insert_str(char_idx, insert); + // Clamp column + let col_idx = col_idx.min(line_len); - lines.join("\n") + // Final insertion char index + let insert_idx = line_start + col_idx; + + rope.insert(insert_idx, insert); + + rope.to_string() } fn find_row_col(output: &str, inject: &str) -> Option<(usize, usize)> { - // 1. Find the flat byte index just like your original code + let rope = Rope::from_str(output); + let byte_idx = output.find(inject)?; - // 2. Slice the string up to the match point - let prefix = &output[..byte_idx]; + // Convert byte index -> char index + let char_idx = output[..byte_idx].chars().count(); - // 3. Row = number of newlines found before the match + 1 (1-indexed) - let row = prefix.lines().count(); + // Ropey line lookup + let line_idx = rope.char_to_line(char_idx); - // 4. Column = character count of the remaining text on the current line + 1 - // (Using .chars().count() ensures it works with multi-byte UTF-8 symbols) - let col = prefix.lines().last().unwrap_or("").chars().count() + 1; + // Line start char index + let line_start = rope.line_to_char(line_idx); - Some((row, col)) + // Column within line + let col_idx = char_idx - line_start; + + // Return 1-indexed + Some((line_idx + 1, col_idx + 1)) } From cdffc8716121e7980492847c943bc0b1535e96e0 Mon Sep 17 00:00:00 2001 From: Selimaj Dev Date: Sun, 24 May 2026 16:49:53 +0200 Subject: [PATCH 5/8] Using ropey --- crates/analyzer/src/lib.rs | 216 +++++++++++++++++++------------------ 1 file changed, 112 insertions(+), 104 deletions(-) diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index 7397b4c..d447b4d 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -3,7 +3,6 @@ pub mod rust_analyzer; pub mod transpiler; use std::collections::{HashMap, HashSet}; -use std::fs; use std::path::{Component, PathBuf}; use std::sync::Arc; @@ -25,6 +24,7 @@ struct Backend { previous_diagnostics: Arc>>>, rust_analyzer: Arc>, mapping: Arc>>>, + documents: Arc>>, } /// Helper function to force percent-encoding on Windows drive colons @@ -263,13 +263,22 @@ impl LanguageServer for Backend { async fn did_open(&self, mut params: DidOpenTextDocumentParams) { if params.text_document.language_id == "mist" { + let original_text = params.text_document.text.clone(); + + self.documents.lock().await.insert( + params.text_document.uri.to_file_path().unwrap(), + Rope::from_str(&original_text), + ); + params.text_document.language_id = "rust".to_string(); - match transpiler::transpile_text(¶ms.text_document.text) { + match transpiler::transpile_text(&original_text) { Ok(transpiled_text) => { params.text_document.text = transpiled_text; + let rust_path = from_mist_to_rust(params.text_document.uri.to_file_path().unwrap()); + params.text_document.uri = Url::from_file_path(&rust_path).unwrap(); self.mapping.lock().await.insert( @@ -279,8 +288,12 @@ impl LanguageServer for Backend { } Err(e) => { self.client - .log_message(MessageType::WARNING, format!("MIST-LSP: Syntax invalid during open/change. Parsing stopped: {:?}", e)) + .log_message( + MessageType::WARNING, + format!("MIST-LSP: Syntax invalid during open/change: {:?}", e), + ) .await; + return; } } @@ -293,6 +306,72 @@ impl LanguageServer for Backend { } } + async fn did_change(&self, mut params: DidChangeTextDocumentParams) { + let mist_path = params.text_document.uri.to_file_path().unwrap(); + + let rust_path = from_mist_to_rust(mist_path.clone()); + + let rust_uri = Url::from_file_path(&rust_path).unwrap(); + + if let Some(change) = params.content_changes.first_mut() { + self.documents + .lock() + .await + .insert(mist_path, Rope::from_str(&change.text)); + + match transpiler::transpile_text(&change.text) { + Ok(transpiled_text) => { + change.text = transpiled_text; + + self.mapping + .lock() + .await + .insert(rust_path, rev_mapper::get_mapping(&change.text)); + } + Err(e) => { + self.client + .log_message( + MessageType::WARNING, + format!("MIST-LSP: Syntax invalid during change: {:?}", e), + ) + .await; + + return; + } + } + } + + params.text_document.uri = rust_uri; + + if let Ok(mut ra) = self.rust_analyzer.try_lock() { + let _ = ra + .notify(notification::DidChangeTextDocument::METHOD, params) + .await; + } + } + + async fn did_close(&self, mut params: DidCloseTextDocumentParams) { + self.client + .log_message(MessageType::INFO, "MIST-LSP: Processing did_close event") + .await; + + let rust_path = from_mist_to_rust(params.text_document.uri.to_file_path().unwrap()); + + let mist_path = params.text_document.uri.to_file_path().unwrap(); + + self.documents.lock().await.remove(&mist_path); + + self.mapping.lock().await.remove(&rust_path); + + params.text_document.uri = Url::from_file_path(&rust_path).unwrap(); + + if let Ok(mut ra) = self.rust_analyzer.try_lock() { + let _ = ra + .notify(notification::DidCloseTextDocument::METHOD, params) + .await; + } + } + async fn goto_definition( &self, params: GotoDefinitionParams, @@ -304,9 +383,9 @@ impl LanguageServer for Backend { .to_file_path() .unwrap(); - let source = match fs::read_to_string(&file_path) { - Ok(src) => src, - Err(_) => return Ok(None), + let source = match self.documents.lock().await.get(&file_path) { + Some(doc) => doc.clone(), + None => return Ok(None), }; let inject = "__mist_23"; @@ -317,10 +396,12 @@ impl LanguageServer for Backend { &inject, ); - let output = match transpiler::transpile_text(&injected_source) { - Ok(out) => out, - Err(_) => return Ok(None), - }; + let output = Rope::from_str( + &match transpiler::transpile_text(&injected_source.to_string()) { + Ok(out) => out, + Err(_) => return Ok(None), + }, + ); let (line, character) = match find_row_col(&output, inject) { Some(coords) => coords, @@ -361,71 +442,6 @@ impl LanguageServer for Backend { }) } - async fn did_change(&self, mut params: DidChangeTextDocumentParams) { - self.client - .log_message(MessageType::INFO, "MIST-LSP: Processing did_change event") - .await; - - let rust_path = from_mist_to_rust(params.text_document.uri.to_file_path().unwrap()); - let rust_uri = Url::from_file_path(&rust_path).unwrap(); - - if let Some(change) = params.content_changes.first_mut() { - match transpiler::transpile_text(&change.text) { - Ok(transpiled_text) => { - change.text = transpiled_text; - - self.mapping - .lock() - .await - .insert(rust_path, rev_mapper::get_mapping(&change.text)); - } - Err(e) => { - self.client - .log_message( - MessageType::WARNING, - format!( - "MIST-LSP: Syntax invalid during change. Sync stopped: {:?}", - e - ), - ) - .await; - return; - } - } - } - - // 3. Re-route URI path to mirror directory - params.text_document.uri = rust_uri; - - // 4. Notify downstream rust-analyzer - if let Ok(mut ra) = self.rust_analyzer.try_lock() { - let _ = ra - .notify(notification::DidChangeTextDocument::METHOD, params) - .await; - } - } - - async fn did_close(&self, mut params: DidCloseTextDocumentParams) { - self.client - .log_message(MessageType::INFO, "MIST-LSP: Processing did_close event") - .await; - - let rust_path = from_mist_to_rust(params.text_document.uri.to_file_path().unwrap()); - - // 1. Evict the mapping from memory to prevent leaks - self.mapping.lock().await.remove(&rust_path); - - // 2. Re-route URI path to mirror directory - params.text_document.uri = Url::from_file_path(&rust_path).unwrap(); - - // 3. Notify downstream rust-analyzer - if let Ok(mut ra) = self.rust_analyzer.try_lock() { - let _ = ra - .notify(notification::DidCloseTextDocument::METHOD, params) - .await; - } - } - async fn completion(&self, mut params: CompletionParams) -> Result> { self.client .log_message(MessageType::INFO, "COMPLETEING!") @@ -438,9 +454,9 @@ impl LanguageServer for Backend { .to_file_path() .unwrap(); - let source = match fs::read_to_string(&file_path) { - Ok(src) => src, - Err(_) => return Ok(None), + let source = match self.documents.lock().await.get(&file_path) { + Some(doc) => doc.clone(), + None => return Ok(None), }; let inject = "__mist_23"; @@ -451,10 +467,12 @@ impl LanguageServer for Backend { &inject, ); - let output = match transpiler::transpile_text(&injected_source) { - Ok(out) => out, - Err(_) => return Ok(None), - }; + let output = Rope::from_str( + &match transpiler::transpile_text(&injected_source.to_string()) { + Ok(out) => out, + Err(_) => return Ok(None), + }, + ); let (line, character) = match find_row_col(&output, inject) { Some(coords) => coords, @@ -490,6 +508,7 @@ pub async fn start() { workspace_folder: Arc::new(Mutex::new(None)), previous_diagnostics: Arc::new(Mutex::new(HashMap::new())), mapping: Arc::new(Mutex::new(HashMap::new())), + documents: Arc::new(Mutex::new(HashMap::new())), rust_analyzer: Arc::new(Mutex::new( RustAnalyzer::new().expect("Failed to create rust analyzer"), )), @@ -539,51 +558,40 @@ pub fn from_rust_to_mist(mut path: PathBuf) -> PathBuf { } } -fn insert_at_position(s: &str, line: usize, col: usize, insert: &str) -> String { - let mut rope = Rope::from_str(s); +fn insert_at_position(rope: &Rope, line: usize, col: usize, insert: &str) -> Rope { + let mut rope = rope.clone(); - // Convert to 0-indexed let line_idx = line.saturating_sub(1); let col_idx = col.saturating_sub(1); - // Clamp line let line_idx = line_idx.min(rope.len_lines().saturating_sub(1)); - // Get start char index of the line let line_start = rope.line_to_char(line_idx); - // Get line length in chars - let line = rope.line(line_idx); - let line_len = line.len_chars(); + let line_slice = rope.line(line_idx); + let line_len = line_slice.len_chars(); - // Clamp column let col_idx = col_idx.min(line_len); - // Final insertion char index - let insert_idx = line_start + col_idx; + let idx = line_start + col_idx; - rope.insert(insert_idx, insert); + rope.insert(idx, insert); - rope.to_string() + rope } -fn find_row_col(output: &str, inject: &str) -> Option<(usize, usize)> { - let rope = Rope::from_str(output); +fn find_row_col(rope: &Rope, needle: &str) -> Option<(usize, usize)> { + let text = rope.to_string(); - let byte_idx = output.find(inject)?; + let byte_idx = text.find(needle)?; - // Convert byte index -> char index - let char_idx = output[..byte_idx].chars().count(); + let char_idx = text[..byte_idx].chars().count(); - // Ropey line lookup let line_idx = rope.char_to_line(char_idx); - // Line start char index let line_start = rope.line_to_char(line_idx); - // Column within line let col_idx = char_idx - line_start; - // Return 1-indexed Some((line_idx + 1, col_idx + 1)) } From dbf3a8038789f0f86355525ac6a87427d5184b8c Mon Sep 17 00:00:00 2001 From: Selimaj Dev Date: Sun, 24 May 2026 17:04:08 +0200 Subject: [PATCH 6/8] Working completions --- crates/analyzer/src/lib.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index d447b4d..9b45f92 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -494,7 +494,15 @@ impl LanguageServer for Backend { .await .expect("Failed to send to rust"); - Ok(rs_res) + Ok(rs_res.map(|rs_res| match rs_res { + CompletionResponse::Array(items) => { + CompletionResponse::Array(items.into_iter().map(simplify_item).collect()) + } + + CompletionResponse::List(list) => { + CompletionResponse::Array(list.items.into_iter().map(simplify_item).collect()) + } + })) } } @@ -595,3 +603,16 @@ fn find_row_col(rope: &Rope, needle: &str) -> Option<(usize, usize)> { Some((line_idx + 1, col_idx + 1)) } + +fn simplify_item(mut item: CompletionItem) -> CompletionItem { + item.text_edit = None; + item.additional_text_edits = None; + item.command = None; + item.data = None; + + item.insert_text = Some(item.label.clone()); + + item.insert_text_format = Some(InsertTextFormat::PLAIN_TEXT); + + item +} From 24134ec9258ed2219575a2db9a331649bcdcf423 Mon Sep 17 00:00:00 2001 From: Selimaj Dev Date: Sun, 24 May 2026 17:24:37 +0200 Subject: [PATCH 7/8] Production safe analyzer --- crates/analyzer/src/lib.rs | 22 ++- crates/analyzer/src/rust_analyzer.rs | 212 +++++++++++++++++---------- 2 files changed, 150 insertions(+), 84 deletions(-) diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index 9b45f92..3362428 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -418,7 +418,7 @@ impl LanguageServer for Backend { .request::(lsp_types::GotoDefinitionParams { text_document_position_params: lsp_types::TextDocumentPositionParams { position: lsp_types::Position { - line: line as u32 - 1, + line: line as u32, character: character as u32, }, text_document: lsp_types::TextDocumentIdentifier { uri }, @@ -504,6 +504,19 @@ impl LanguageServer for Backend { } })) } + + async fn completion_resolve(&self, params: CompletionItem) -> Result { + match self + .rust_analyzer + .lock() + .await + .request::(params.clone()) + .await + { + Ok(o) => Ok(o), + _ => Ok(params), + } + } } #[tokio::main] @@ -601,18 +614,13 @@ fn find_row_col(rope: &Rope, needle: &str) -> Option<(usize, usize)> { let col_idx = char_idx - line_start; - Some((line_idx + 1, col_idx + 1)) + Some((line_idx, col_idx + 1)) } fn simplify_item(mut item: CompletionItem) -> CompletionItem { item.text_edit = None; item.additional_text_edits = None; item.command = None; - item.data = None; - - item.insert_text = Some(item.label.clone()); - - item.insert_text_format = Some(InsertTextFormat::PLAIN_TEXT); item } diff --git a/crates/analyzer/src/rust_analyzer.rs b/crates/analyzer/src/rust_analyzer.rs index b7e584d..ec92b8e 100644 --- a/crates/analyzer/src/rust_analyzer.rs +++ b/crates/analyzer/src/rust_analyzer.rs @@ -1,10 +1,11 @@ -use std::{collections::HashMap, path::PathBuf, process::Stdio, sync::Arc}; +use std::{collections::HashMap, path::PathBuf, process::Stdio, sync::Arc, time::Duration}; use serde::{Deserialize, Serialize, de::DeserializeOwned}; use serde_json::{Value, json}; use tokio::{ io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}, sync::{Mutex, oneshot}, + time::timeout, }; use tower_lsp::lsp_types::{ self, ClientCapabilities, InitializeParams, InitializedParams, Url, WorkspaceFolder, @@ -12,10 +13,13 @@ use tower_lsp::lsp_types::{ request::{self, Request}, }; +const MAX_CONTENT_LENGTH: usize = 50 * 1024 * 1024; +const REQUEST_TIMEOUT: Duration = Duration::from_secs(30); + #[derive(Debug, Deserialize)] pub struct JsonRpcResponse { pub jsonrpc: String, - pub id: Option, + pub id: Option, pub result: Option, pub error: Option, } @@ -27,13 +31,16 @@ pub struct JsonRpcError { pub data: Option, } -type PendingMap = Arc>>>; +type PendingMap = Arc>>>>; #[derive(Debug)] pub struct RustAnalyzer { - stdin: tokio::process::ChildStdin, + // Wrapped in a Mutex to support safe concurrent sharing if the design expands + stdin: Arc>, pending: PendingMap, id: usize, + // Keep child handler to explicitly manage child process lifecycle and prevent zombie processes + _child: tokio::process::Child, } async fn send_lsp_message( @@ -49,27 +56,37 @@ async fn send_lsp_message( async fn read_lsp_message( reader: &mut R, -) -> Result> { +) -> Result> { let mut line = String::new(); let mut content_length = 0; - // Read headers until we hit the empty separator line (\r\n) - loop { + // Guard against infinite header reading attacks/bugs (Max 100 headers) + for _ in 0..100 { line.clear(); - reader.read_line(&mut line).await?; - if line == "\r\n" || line.is_empty() { + let bytes_read = reader.read_line(&mut line).await?; + if bytes_read == 0 || line == "\r\n" || line.is_empty() { break; } - if line.to_lowercase().starts_with("content-length:") { - content_length = line["content-length:".len()..].trim().parse::()?; + if line.to_ascii_lowercase().starts_with("content-length:") { + if let Some(val_str) = line.split(':').nth(1) { + content_length = val_str.trim().parse::()?; + } } } + // Explode early if payload size violates strict guard rails to prevent memory-exhaustion (OOM) if content_length == 0 { - return Err("Missing or invalid Content-Length header".into()); + return Err("Missing, invalid, or zero Content-Length header".into()); + } + if content_length > MAX_CONTENT_LENGTH { + return Err(format!( + "Content-Length {} exceeds maximum threshold", + content_length + ) + .into()); } - // Read the exact byte buffer payload + // Explicitly secure pre-allocation limit let mut buffer = vec![0u8; content_length]; reader.read_exact(&mut buffer).await?; @@ -77,19 +94,26 @@ async fn read_lsp_message( } impl RustAnalyzer { - pub fn new() -> Result> { + pub fn new() -> Result> { let mut child = tokio::process::Command::new("rust-analyzer") .stdin(Stdio::piped()) .stdout(Stdio::piped()) - .stderr(Stdio::null()) // Ignore logs for simplicity + .stderr(Stdio::null()) .spawn()?; - let stdin = child.stdin.take().unwrap(); - let stdout = child.stdout.take().unwrap(); + let stdin = child + .stdin + .take() + .ok_or("Failed to open child stdin pipe")?; + let stdout = child + .stdout + .take() + .ok_or("Failed to open child stdout pipe")?; let pending: PendingMap = Arc::new(Mutex::new(HashMap::new())); let pending_clone = pending.clone(); + // Background supervisor task loop tokio::spawn(async move { let mut stdout = BufReader::new(stdout); @@ -97,7 +121,12 @@ impl RustAnalyzer { let raw = match read_lsp_message(&mut stdout).await { Ok(v) => v, Err(err) => { - eprintln!("LSP read error: {err}"); + eprintln!("LSP fatal stream read failure: {err}"); + // CRITICAL: Notify all pending channels that the bridge broke down + let mut lock = pending_clone.lock().await; + for (_, tx) in lock.drain() { + let _ = tx.send(Err(format!("LSP reader task dropped: {}", err))); + } break; } }; @@ -105,51 +134,43 @@ impl RustAnalyzer { let value: Value = match serde_json::from_str(&raw) { Ok(v) => v, Err(err) => { - eprintln!("Invalid JSON from rust-analyzer: {err}"); - continue; + eprintln!("Corrupted JSON received: {err}"); + continue; // Keep the connection running despite malformed frame } }; - if value.get("method").is_some() { + // Filter server notification frames + if value.get("method").is_some() && value.get("id").is_none() { continue; } let id = value.get("id").and_then(|v| v.as_u64()).map(|v| v as usize); - match id { - Some(id) => { - let tx = pending_clone.lock().await.remove(&id); - - match tx { - Some(tx) => { - let _ = tx.send(value); - } - None => { - eprintln!("({:?}) {}", pending_clone, value); - eprintln!("Received response for unknown request id {id}"); - } - } - } - - None => { - // notification or server request - eprintln!("Received server notification/request: {raw}"); + if let Some(id) = id { + let tx = pending_clone.lock().await.remove(&id); + if let Some(tx) = tx { + let _ = tx.send(Ok(value)); + } else { + eprintln!("Received orphaned or delayed frame for ID: {id}"); } + } else { + eprintln!("Received unhandled protocol notification framework: {raw}"); } } }); Ok(Self { - stdin, + stdin: Arc::new(Mutex::new(stdin)), pending, id: 0, + _child: child, }) } pub async fn request( &mut self, params: R::Params, - ) -> Result> + ) -> Result> where R::Result: DeserializeOwned, { @@ -160,47 +181,86 @@ impl RustAnalyzer { let (tx, rx) = oneshot::channel(); - self.pending.lock().await.insert(id, tx); - - send_lsp_message( - &mut self.stdin, - &json!({ - "jsonrpc": "2.0", - "id": id, - "method": R::METHOD, - "params": params, - }), - ) - .await?; - - let value = rx.await?; - - let envelope: JsonRpcResponse = serde_json::from_value(value.clone())?; - - if let Some(err) = envelope.error { - return Err(format!("LSP Error ({}): {}", err.code, err.message).into()); + // Scope the lock allocation tightly + { + self.pending.lock().await.insert(id, tx); } - envelope - .result - .ok_or_else(|| format!("missing result: {:?}", value).into()) + let payload = json!({ + "jsonrpc": "2.0", + "id": id, + "method": R::METHOD, + "params": params, + }); + + // Acquire lock on writing stream to ensure thread safety + let mut stdin_lock = self.stdin.lock().await; + + if let Err(err) = send_lsp_message(&mut *stdin_lock, &payload).await { + // Rollback the pending map operation to avoid internal memory memory-leaks if serialization/IO errors trigger + self.pending.lock().await.remove(&id); + return Err(Box::new(err)); + } + + // Explicit drop of write lock early so other operations can pipe messages synchronously + drop(stdin_lock); + + // Enforce an absolute time constraint limit to break free from hanging processes + let response_payload = match timeout(REQUEST_TIMEOUT, rx).await { + Ok(Ok(Ok(value))) => value, + Ok(Ok(Err(task_err))) => return Err(task_err.into()), + Ok(Err(_oneshot_canceled)) => { + return Err( + "Bridge connection closed down; reader channel dropped unexpectedly".into(), + ); + } + Err(_timeout_elapsed) => { + // Clear state tracking entries dynamically upon expiration failure + self.pending.lock().await.remove(&id); + return Err( + format!("Request ID {} timed out after {:?}", id, REQUEST_TIMEOUT).into(), + ); + } + }; + + let envelope: JsonRpcResponse = + serde_json::from_value(response_payload.clone())?; + + if let Some(err) = envelope.error { + return Err(format!("LSP Engine Error ({}): {}", err.code, err.message).into()); + } + + envelope.result.ok_or_else(|| { + format!( + "Missing inner structural payload result: {:?}", + response_payload + ) + .into() + }) } - pub async fn notify(&mut self, method: &str, req: T) -> std::io::Result<()> { - send_lsp_message( - &mut self.stdin, - &json!({ - "jsonrpc": "2.0", - "method": method, - "params": req, - }), - ) - .await + pub async fn notify( + &mut self, + method: &str, + req: T, + ) -> Result<(), Box> { + let payload = json!({ + "jsonrpc": "2.0", + "method": method, + "params": req, + }); + + let mut stdin_lock = self.stdin.lock().await; + send_lsp_message(&mut *stdin_lock, &payload).await?; + Ok(()) } } impl RustAnalyzer { - pub async fn initialize(&mut self, root: &PathBuf) -> Result<(), Box> { + pub async fn initialize( + &mut self, + root: &PathBuf, + ) -> Result<(), Box> { let project_uri = Url::from_directory_path(root) .map_err(|_| "Failed to convert path to valid file:// URL")?; @@ -226,14 +286,12 @@ impl RustAnalyzer { }; self.request::(init_params).await?; - Ok(()) } - pub async fn initialized(&mut self) -> Result<(), Box> { + pub async fn initialized(&mut self) -> Result<(), Box> { self.notify(Initialized::METHOD, InitializedParams {}) .await?; - Ok(()) } } From ecdbc9416a9a4985eb5834064a816c9b7718825d Mon Sep 17 00:00:00 2001 From: Selimaj Dev Date: Sun, 24 May 2026 17:38:35 +0200 Subject: [PATCH 8/8] Build all on initialize --- crates/analyzer/src/lib.rs | 48 +++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index 3362428..5e0a856 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -134,8 +134,14 @@ impl LanguageServer for Backend { let analyzer = self.rust_analyzer.clone(); + let documents = self.documents.clone(); + + let mapping = self.mapping.clone(); + tokio::spawn(async move { if let Some(root) = &*workspace_folder.lock().await { + let src_root = root.join("src"); + transpiler::build(root); analyzer @@ -145,6 +151,30 @@ impl LanguageServer for Backend { .await .expect("Failed to initialize rust analyzer"); + // ---- LOAD ALL .MIST FILES ---- + let mut files = Vec::new(); + collect_mist_files(&src_root, &mut files); + + for file in files { + if let Ok(text) = std::fs::read_to_string(&file) { + if let Ok(transpiled) = transpiler::transpile_text(&text) { + let rust_path = from_mist_to_rust(file.clone()); + + // store document + documents + .lock() + .await + .insert(file.clone(), Rope::from_str(&text)); + + // store mapping + mapping + .lock() + .await + .insert(rust_path, rev_mapper::get_mapping(&transpiled)); + } + } + } + eprintln!("Ready to use"); } }); @@ -227,7 +257,7 @@ impl LanguageServer for Backend { }, end: Position { line, - character: column + 1, + character: u32::MAX, }, }, severity: Some(severity), @@ -624,3 +654,19 @@ fn simplify_item(mut item: CompletionItem) -> CompletionItem { item } + +fn collect_mist_files(dir: &PathBuf, out: &mut Vec) { + let Ok(entries) = std::fs::read_dir(dir) else { + return; + }; + + for entry in entries.flatten() { + let path = entry.path(); + + if path.is_dir() { + collect_mist_files(&path, out); + } else if path.extension().and_then(|e| e.to_str()) == Some("mist") { + out.push(path); + } + } +}