diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index 14473d2..f60c225 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -1,17 +1,20 @@ pub mod rust_analyzer; +pub mod transpiler; -use std::collections::{HashMap, HashSet}; -use std::path::{Component, PathBuf}; +use std::collections::HashMap; +use std::path::{Component, Path, PathBuf}; use std::sync::Arc; -use mist_parser::rev_mapper; +use mist_parser::rev_mapper::{Mapping, MistMap, RustMap}; use ropey::Rope; +use serde::Deserialize; +use serde_json::Value; use tokio::sync::Mutex; -use tower_lsp::jsonrpc::Result; use tower_lsp::lsp_types::{self, *}; use tower_lsp::{Client, LanguageServer, LspService, Server}; use crate::rust_analyzer::RustAnalyzer; +use crate::transpiler::transpile_mist; #[derive(Debug)] struct Backend { @@ -19,23 +22,21 @@ struct Backend { workspace_folder: Arc>>, previous_diagnostics: Arc>>>, rust_analyzer: Arc>, - mapping: Arc>>>, + mapping: Arc>>, documents: Arc>>, + doc_versions: Arc>>, + notification_rx: Arc>>>, } -/// Helper function to force percent-encoding on Windows drive colons -/// so that the URLs exactly match what VS Code/LSP clients send. fn clean_lsp_url(path: &std::path::Path) -> Option { let mut url_str = Url::from_file_path(path).ok()?.to_string(); - // Look for Windows patterns like file:///D: or file:///d: and switch to %3A - if url_str.starts_with("file:///I:") || url_str.starts_with("file:///i:") || // Catch-all or explicit check: - (url_str.len() > 10 && url_str.as_bytes()[11] == b':') + if url_str.starts_with("file:///I:") + || url_str.starts_with("file:///i:") + || (url_str.len() > 10 && url_str.as_bytes()[11] == b':') { - // Safely replace the first colon occurring after "file:///" if let Some(pos) = url_str.find(':') { if pos == 11 { - // Double check it's the drive letter colon url_str.replace_range(pos..=pos, "%3A"); } } @@ -44,48 +45,183 @@ fn clean_lsp_url(path: &std::path::Path) -> Option { Url::parse(&url_str).ok() } +fn lsp_pos_to_rust_map(pos: &Position) -> RustMap { + RustMap(pos.line as usize + 1, pos.character as usize) +} + +fn rust_map_to_lsp_pos(map: &RustMap) -> Position { + Position { + line: map.0.saturating_sub(1) as u32, + character: map.1 as u32, + } +} + +fn lsp_pos_to_mist_map(pos: &Position) -> MistMap { + MistMap(pos.line as usize + 1, pos.character as usize) +} + +fn mist_map_to_lsp_pos(map: &MistMap) -> Position { + Position { + line: map.0.saturating_sub(1) as u32, + character: map.1 as u32, + } +} + +fn mist_to_rust_path(mist_path: &Path) -> PathBuf { + let mut path = mist_path.to_path_buf(); + path.set_extension("rs"); + + let comps: Vec = path.components().collect(); + if let Some(pos) = comps.iter().rposition(|c| c.as_os_str() == "src") { + let replacement = Path::new(".mist/src"); + let mut new_comps: Vec = comps[..pos].to_vec(); + new_comps.extend(replacement.components()); + new_comps.extend(&comps[pos + 1..]); + new_comps.iter().collect() + } else { + path + } +} + +fn rust_to_mist_path(rust_path: &Path) -> PathBuf { + let mut path = rust_path.to_path_buf(); + path.set_extension("mist"); + + let comps: Vec = path.components().collect(); + + let pattern: Vec = Path::new(".mist/src").components().collect(); + + if let Some(pos) = comps + .windows(pattern.len()) + .rposition(|window| window == pattern.as_slice()) + { + let mut new_comps = comps.clone(); + new_comps.splice( + pos..pos + pattern.len(), + std::iter::once(Component::Normal(std::ffi::OsStr::new("src"))), + ); + new_comps.iter().collect() + } else { + path + } +} + +fn mist_uri_to_rust_uri(mist_uri: &Url) -> Option { + let mist_path = mist_uri.to_file_path().ok()?; + let rust_path = mist_to_rust_path(&mist_path); + clean_lsp_url(&rust_path) +} + +fn rust_uri_to_mist_uri(rust_uri: &Url) -> Option { + let rust_path = rust_uri.to_file_path().ok()?; + let mist_path = rust_to_mist_path(&rust_path); + clean_lsp_url(&mist_path) +} + impl Backend { - async fn get_mist_location(&self, rs_loc: Location) -> Location { - let rs_path = match rs_loc.uri.to_file_path() { - Ok(path) => path, - Err(_) => return rs_loc, + async fn map_mist_to_rust_pos( + &self, + mist_uri: &Url, + mist_pos: &Position, + ) -> Option<(Url, Position)> { + let rust_uri = mist_uri_to_rust_uri(mist_uri)?; + let rust_path = rust_uri.to_file_path().ok()?; + + let mist_target = lsp_pos_to_mist_map(mist_pos); + let mapping = self.mapping.lock().await.get(&rust_path)?.clone(); + let (rust_map, _) = mapping.find_by_mist(&mist_target)?; + + let pos = rust_map_to_lsp_pos(&rust_map); + Some((rust_uri, pos)) + } + + async fn map_rust_to_mist_pos( + &self, + rust_uri: &Url, + rust_pos: &Position, + ) -> Option<(Url, Position)> { + let mist_uri = rust_uri_to_mist_uri(rust_uri)?; + let rust_path = rust_uri.to_file_path().ok()?; + + let rust_target = lsp_pos_to_rust_map(rust_pos); + let mapping = self.mapping.lock().await.get(&rust_path)?.clone(); + let (_, mist_map) = mapping.find(&rust_target)?; + + let pos = mist_map_to_lsp_pos(&mist_map); + Some((mist_uri, pos)) + } + + async fn handle_transpile_and_notify(&self, mist_path: &Path, source: &str) { + let transpiled = match transpile_mist(mist_path, source) { + Ok(t) => t, + Err(e) => { + eprintln!("transpile error for {:?}: {e}", mist_path); + let diag = Diagnostic { + range: Range { + start: Position { + line: 0, + character: 0, + }, + end: Position { + line: 0, + character: 1, + }, + }, + severity: Some(DiagnosticSeverity::ERROR), + source: Some("mist".to_string()), + message: format!("Transpile error: {e}"), + ..Default::default() + }; + if let Some(uri) = clean_lsp_url(mist_path) { + self.publish_diagnostics(uri, vec![diag]).await; + } + return; + } }; - match self.mapping.lock().await.get(&rs_path) { - Some(mapping) => { - rs_loc - // FIX: Match the find_mapping operation instead of calling .expect("Failed to map") - // match rev_mapper::find_mapping( - // mapping, - // &rev_mapper::RustMap( - // rs_loc.range.start.line as usize, - // rs_loc.range.start.character as usize, - // ), - // ) { - // Some((_, rev_mapper::MistMap(line, character))) => Location { - // uri: Url::from_file_path(from_rust_to_mist(rs_path)).unwrap(), - // range: Range { - // start: Position { - // line: line as u32 - 1, - // character: character as u32, - // }, - // end: Position { - // line: line as u32 - 1, - // character: character as u32 + 1, - // }, - // }, - // }, - // None => rs_loc, - // } + if let Some(rust_uri) = clean_lsp_url(&transpiled.rust_path) { + let mut ra = self.rust_analyzer.lock().await; + let mut map = self.mapping.lock().await; + let mut versions = self.doc_versions.lock().await; + + let version = versions.entry(mist_path.to_path_buf()).or_insert(0); + *version += 1; + let current_version = *version; + + if map.contains_key(&transpiled.rust_path) { + let _ = ra + .did_change(rust_uri, &transpiled.rust_content, current_version) + .await; + } else { + let _ = ra.did_open(rust_uri, &transpiled.rust_content).await; } - None => rs_loc, + + map.insert(transpiled.rust_path, transpiled.mapping); + } + } + + async fn publish_diagnostics(&self, uri: Url, diagnostics: Vec) { + let key = uri.clone(); + let prev = self.previous_diagnostics.lock().await.get(&key).cloned(); + + if prev.as_ref() != Some(&diagnostics) { + self.previous_diagnostics + .lock() + .await + .insert(key, diagnostics.clone()); + self.client + .publish_diagnostics(uri, diagnostics, None) + .await; } } } #[tower_lsp::async_trait] impl LanguageServer for Backend { - async fn initialize(&self, params: InitializeParams) -> Result { + async fn initialize( + &self, + params: InitializeParams, + ) -> tower_lsp::jsonrpc::Result { let mut res = InitializeResult::default(); res.capabilities.text_document_sync = Some(TextDocumentSyncCapability::Options( @@ -99,7 +235,7 @@ impl LanguageServer for Backend { )); res.capabilities.completion_provider = Some(CompletionOptions { - resolve_provider: Some(true), + resolve_provider: Some(false), trigger_characters: Some(vec![ ":".to_owned(), ".".to_owned(), @@ -116,6 +252,7 @@ impl LanguageServer for Backend { }); res.capabilities.definition_provider = Some(OneOf::Left(true)); + res.capabilities.hover_provider = Some(HoverProviderCapability::Simple(true)); let folder_path = params .workspace_folders @@ -127,31 +264,79 @@ impl LanguageServer for Backend { *self.workspace_folder.lock().await = Some(path.clone()); } - let workspace_folder = self.workspace_folder.clone(); - - let analyzer = self.rust_analyzer.clone(); - - let documents = self.documents.clone(); - + let ws = self.workspace_folder.clone(); + let ra = self.rust_analyzer.clone(); let mapping = self.mapping.clone(); + let documents = self.documents.clone(); + let client = self.client.clone(); + let previous_diagnostics = self.previous_diagnostics.clone(); + let notification_rx = self.notification_rx.clone(); tokio::spawn(async move { - if let Some(root) = &*workspace_folder.lock().await { + if let Some(root) = &*ws.lock().await { let src_root = root.join("src"); - analyzer - .lock() - .await - .initialize(root) - .await - .expect("Failed to initialize rust analyzer"); + if let Err(e) = ra.lock().await.initialize(root).await { + eprintln!("Failed to initialize rust-analyzer: {e}"); + return; + } + + if let Err(e) = ra.lock().await.initialized().await { + eprintln!("rust-analyzer initialized failed: {e}"); + return; + } - // ---- 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) {} + for file in &files { + if let Ok(text) = std::fs::read_to_string(file) { + documents + .lock() + .await + .insert(file.clone(), Rope::from_str(&text)); + } + } + + eprintln!("Loaded {} mist files", files.len()); + + let mapping_c = mapping.clone(); + let documents_c = documents.clone(); + let ra_c = ra.clone(); + + for file in &files { + if let Some(source) = documents_c.lock().await.get(file).map(|r| r.to_string()) + { + let transpiled = match transpile_mist(file, &source) { + Ok(t) => t, + Err(e) => { + eprintln!("transpile error for {:?}: {e}", file); + continue; + } + }; + + if let Some(rust_uri) = clean_lsp_url(&transpiled.rust_path) { + let _ = ra_c + .lock() + .await + .did_open(rust_uri, &transpiled.rust_content) + .await; + mapping_c + .lock() + .await + .insert(transpiled.rust_path, transpiled.mapping); + } + } + } + + if let Some(rx) = notification_rx.lock().await.take() { + tokio::spawn(handle_ra_notifications( + rx, + client, + mapping, + documents, + previous_diagnostics, + )); } eprintln!("Ready to use"); @@ -162,21 +347,387 @@ impl LanguageServer for Backend { } async fn initialized(&self, _: InitializedParams) { - 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<()> { + async fn shutdown(&self) -> tower_lsp::jsonrpc::Result<()> { Ok(()) } + + async fn did_open(&self, params: DidOpenTextDocumentParams) { + let uri = params.text_document.uri; + let mist_path = match uri.to_file_path() { + Ok(p) => p, + Err(_) => { + return; + } + }; + let source = params.text_document.text; + + self.documents + .lock() + .await + .insert(mist_path.clone(), Rope::from_str(&source)); + + self.handle_transpile_and_notify(&mist_path, &source).await; + } + + async fn did_change(&self, params: DidChangeTextDocumentParams) { + let uri = params.text_document.uri; + let mist_path = match uri.to_file_path() { + Ok(p) => p, + Err(_) => { + return; + } + }; + + if let Some(change) = params.content_changes.into_iter().last() { + let source = change.text; + + self.documents + .lock() + .await + .insert(mist_path.clone(), Rope::from_str(&source)); + + self.handle_transpile_and_notify(&mist_path, &source).await; + } + } + + async fn did_save(&self, params: DidSaveTextDocumentParams) { + let uri = params.text_document.uri; + let mist_path = match uri.to_file_path() { + Ok(p) => p, + Err(_) => { + return; + } + }; + + if let Some(text) = params.text { + self.documents + .lock() + .await + .insert(mist_path.clone(), Rope::from_str(&text)); + + self.handle_transpile_and_notify(&mist_path, &text).await; + } + } + + async fn did_close(&self, params: DidCloseTextDocumentParams) { + let uri = params.text_document.uri; + let mist_path = match uri.to_file_path() { + Ok(p) => p, + Err(_) => { + return; + } + }; + + self.documents.lock().await.remove(&mist_path); + + let rust_path = mist_to_rust_path(&mist_path); + self.mapping.lock().await.remove(&rust_path); + + if let Some(rust_uri) = clean_lsp_url(&rust_path) { + let _ = self.rust_analyzer.lock().await.did_close(rust_uri).await; + } + + self.publish_diagnostics(uri, Vec::new()).await; + } + + async fn completion( + &self, + params: CompletionParams, + ) -> tower_lsp::jsonrpc::Result> { + let mist_uri = params.text_document_position.text_document.uri.clone(); + let mist_pos = params.text_document_position.position; + + let Some((rust_uri, rust_pos)) = self.map_mist_to_rust_pos(&mist_uri, &mist_pos).await + else { + return Ok(None); + }; + + let comp_params = CompletionParams { + text_document_position: TextDocumentPositionParams { + text_document: TextDocumentIdentifier { uri: rust_uri }, + position: rust_pos, + }, + work_done_progress_params: WorkDoneProgressParams { + work_done_token: None, + }, + partial_result_params: PartialResultParams { + partial_result_token: None, + }, + context: params.context, + }; + + match self + .rust_analyzer + .lock() + .await + .request::(comp_params) + .await + { + Ok(Some(CompletionResponse::Array(items))) => { + let cleaned: Vec = + items.into_iter().map(clean_completion_item).collect(); + Ok(Some(CompletionResponse::Array(cleaned))) + } + Ok(Some(CompletionResponse::List(list))) => { + let cleaned: Vec = + list.items.into_iter().map(clean_completion_item).collect(); + Ok(Some(CompletionResponse::List(CompletionList { + is_incomplete: list.is_incomplete, + items: cleaned, + }))) + } + Ok(None) => Ok(None), + Err(e) => { + eprintln!("completion error: {e}"); + Ok(None) + } + } + } + + async fn goto_definition( + &self, + params: GotoDefinitionParams, + ) -> tower_lsp::jsonrpc::Result> { + let mist_uri = params.text_document_position_params.text_document.uri; + let mist_pos = params.text_document_position_params.position; + + let Some((rust_uri, rust_pos)) = self.map_mist_to_rust_pos(&mist_uri, &mist_pos).await + else { + return Ok(None); + }; + + let gd_params = GotoDefinitionParams { + text_document_position_params: TextDocumentPositionParams { + text_document: TextDocumentIdentifier { uri: rust_uri }, + position: rust_pos, + }, + work_done_progress_params: WorkDoneProgressParams { + work_done_token: None, + }, + partial_result_params: PartialResultParams { + partial_result_token: None, + }, + }; + + match self + .rust_analyzer + .lock() + .await + .request::(gd_params) + .await + { + Ok(Some(GotoDefinitionResponse::Scalar(loc))) => { + let mapped = self.map_rust_to_mist_pos(&loc.uri, &loc.range.start).await; + match mapped { + Some((mist_uri, mist_start)) => { + let mist_range = Range { + start: mist_start, + end: Position { + line: mist_start.line, + character: mist_start.character + 1, + }, + }; + Ok(Some(GotoDefinitionResponse::Scalar(Location { + uri: mist_uri, + range: mist_range, + }))) + } + None => Ok(Some(GotoDefinitionResponse::Scalar(loc))), + } + } + Ok(Some(GotoDefinitionResponse::Array(locs))) => { + let mut mapped = Vec::new(); + for loc in locs { + if let Some((mist_uri, mist_start)) = + self.map_rust_to_mist_pos(&loc.uri, &loc.range.start).await + { + mapped.push(Location { + uri: mist_uri, + range: Range { + start: mist_start, + end: Position { + line: mist_start.line, + character: mist_start.character + 1, + }, + }, + }); + } + } + Ok(Some(GotoDefinitionResponse::Array(mapped))) + } + Ok(Some(GotoDefinitionResponse::Link(links))) => { + let mut mapped = Vec::new(); + for link in links { + if let Some((mist_uri, mist_start)) = self + .map_rust_to_mist_pos(&link.target_uri, &link.target_selection_range.start) + .await + { + mapped.push(LocationLink { + origin_selection_range: link.origin_selection_range, + target_uri: mist_uri, + target_range: Range { + start: mist_start, + end: Position { + line: mist_start.line, + character: mist_start.character + 1, + }, + }, + target_selection_range: Range { + start: mist_start, + end: Position { + line: mist_start.line, + character: mist_start.character + 1, + }, + }, + }); + } + } + Ok(Some(GotoDefinitionResponse::Link(mapped))) + } + Ok(None) => Ok(None), + Err(e) => { + eprintln!("goto_definition error: {e}"); + Ok(None) + } + } + } + + async fn hover(&self, params: HoverParams) -> tower_lsp::jsonrpc::Result> { + let mist_uri = params.text_document_position_params.text_document.uri; + let mist_pos = params.text_document_position_params.position; + + let Some((rust_uri, rust_pos)) = self.map_mist_to_rust_pos(&mist_uri, &mist_pos).await + else { + return Ok(None); + }; + + let h_params = HoverParams { + text_document_position_params: TextDocumentPositionParams { + text_document: TextDocumentIdentifier { uri: rust_uri }, + position: rust_pos, + }, + work_done_progress_params: WorkDoneProgressParams { + work_done_token: None, + }, + }; + + match self + .rust_analyzer + .lock() + .await + .request::(h_params) + .await + { + Ok(hover) => Ok(hover), + Err(e) => { + eprintln!("hover error: {e}"); + Ok(None) + } + } + } +} + +async fn handle_ra_notifications( + mut rx: tokio::sync::mpsc::UnboundedReceiver, + client: Client, + mapping: Arc>>, + _documents: Arc>>, + previous_diagnostics: Arc>>>, +) { + #[derive(Deserialize)] + struct PublishDiagnosticsParams { + uri: Url, + diagnostics: Vec, + } + + while let Some(notification) = rx.recv().await { + let method = notification + .get("method") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + match method.as_deref() { + Some("textDocument/publishDiagnostics") => { + if let Ok(params) = serde_json::from_value::( + notification["params"].clone(), + ) { + let rust_uri = params.uri; + let diagnostics = params.diagnostics; + + let mist_uri = rust_uri_to_mist_uri(&rust_uri); + + let mapped_diagnostics: Vec = if let Some(ref _mist_uri) = mist_uri + { + let rust_path = rust_uri.to_file_path().ok(); + let map_data = + rust_path.and_then(|p| mapping.blocking_lock().get(&p).cloned()); + + diagnostics + .into_iter() + .filter_map(|diag| { + let map = map_data.clone()?; + + let rust_start = lsp_pos_to_rust_map(&diag.range.start); + let rust_end = lsp_pos_to_rust_map(&diag.range.end); + + let (_, mist_start) = map.find(&rust_start)?; + let (_, mist_end) = map.find(&rust_end)?; + + let mist_start_pos = mist_map_to_lsp_pos(&mist_start); + let mist_end_pos = mist_map_to_lsp_pos(&mist_end); + + let final_end = Position { + line: mist_end_pos.line.max(mist_start_pos.line), + character: if mist_end_pos.line == mist_start_pos.line { + mist_end_pos.character.max(mist_start_pos.character + 1) + } else { + mist_end_pos.character.max(1) + }, + }; + + Some(Diagnostic { + range: Range { + start: mist_start_pos, + end: final_end, + }, + severity: diag.severity, + code: diag.code, + code_description: diag.code_description, + source: diag.source, + message: diag.message, + related_information: diag.related_information, + tags: diag.tags, + data: diag.data, + }) + }) + .collect() + } else { + diagnostics + }; + + if let Some(mist_uri) = mist_uri.or(Some(rust_uri.clone())) { + let prev = previous_diagnostics.lock().await.get(&mist_uri).cloned(); + + if prev.as_ref() != Some(&mapped_diagnostics) { + previous_diagnostics + .lock() + .await + .insert(mist_uri.clone(), mapped_diagnostics.clone()); + client + .publish_diagnostics(mist_uri, mapped_diagnostics, None) + .await; + } + } + } + } + _ => {} + } + } } #[tokio::main] @@ -184,103 +735,37 @@ pub async fn start() { let stdin = tokio::io::stdin(); let stdout = tokio::io::stdout(); - let (service, socket) = LspService::new(|client| Backend { - client, - 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"), - )), + let (service, socket) = LspService::new(|client| { + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + + Backend { + client, + 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())), + doc_versions: Arc::new(Mutex::new(HashMap::new())), + rust_analyzer: Arc::new(Mutex::new( + RustAnalyzer::new(tx).expect("Failed to create rust analyzer"), + )), + notification_rx: Arc::new(Mutex::new(Some(rx))), + } }); Server::new(stdin, stdout, socket).serve(service).await; } -pub fn from_mist_to_rust(mut path: PathBuf) -> PathBuf { - path.set_extension("rs"); - - let mut comps: Vec = path.components().collect(); - - if let Some(pos) = comps.iter().rposition(|c| c.as_os_str() == "src") { - let replacement = std::path::Path::new(".mist/src"); - comps.splice(pos..=pos, replacement.components()); - comps.iter().collect() - } else { - path - } +pub fn from_mist_to_rust(path: PathBuf) -> PathBuf { + mist_to_rust_path(&path) } -pub fn from_rust_to_mist(mut path: PathBuf) -> PathBuf { - // reverse extension - path.set_extension("mist"); - - let comps: Vec = path.components().collect(); - - let pattern: Vec = std::path::Path::new(".mist/src").components().collect(); - - // find the last occurrence of the pattern - if let Some(pos) = comps - .windows(pattern.len()) - .rposition(|window| window == pattern.as_slice()) - { - let mut new_comps = comps.clone(); - - // replace the matched range with "src" - new_comps.splice( - pos..pos + pattern.len(), - std::iter::once(Component::Normal(std::ffi::OsStr::new("src"))), - ); - - new_comps.iter().collect() - } else { - path - } +pub fn from_rust_to_mist(path: PathBuf) -> PathBuf { + rust_to_mist_path(&path) } -fn insert_at_position(rope: &Rope, line: usize, col: usize, insert: &str) -> Rope { - let mut rope = rope.clone(); - - let line_idx = line.saturating_sub(1); - let col_idx = col.saturating_sub(1); - - let line_idx = line_idx.min(rope.len_lines().saturating_sub(1)); - - let line_start = rope.line_to_char(line_idx); - - let line_slice = rope.line(line_idx); - let line_len = line_slice.len_chars(); - - let col_idx = col_idx.min(line_len); - - let idx = line_start + col_idx; - - rope.insert(idx, insert); - - rope -} - -fn find_row_col(rope: &Rope, needle: &str) -> Option<(usize, usize)> { - let text = rope.to_string(); - - let byte_idx = text.find(needle)?; - - let char_idx = text[..byte_idx].chars().count(); - - let line_idx = rope.char_to_line(char_idx); - - let line_start = rope.line_to_char(line_idx); - - let col_idx = char_idx - line_start; - - Some((line_idx, col_idx + 1)) -} - -fn simplify_item(mut item: CompletionItem) -> CompletionItem { +fn clean_completion_item(mut item: CompletionItem) -> CompletionItem { item.text_edit = None; item.additional_text_edits = None; item.command = None; - item } diff --git a/crates/analyzer/src/rust_analyzer.rs b/crates/analyzer/src/rust_analyzer.rs index ec92b8e..56450cb 100644 --- a/crates/analyzer/src/rust_analyzer.rs +++ b/crates/analyzer/src/rust_analyzer.rs @@ -35,11 +35,9 @@ type PendingMap = Arc #[derive(Debug)] pub struct RustAnalyzer { - // 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, } @@ -60,7 +58,6 @@ async fn read_lsp_message( let mut line = String::new(); let mut content_length = 0; - // Guard against infinite header reading attacks/bugs (Max 100 headers) for _ in 0..100 { line.clear(); let bytes_read = reader.read_line(&mut line).await?; @@ -74,7 +71,6 @@ async fn read_lsp_message( } } - // Explode early if payload size violates strict guard rails to prevent memory-exhaustion (OOM) if content_length == 0 { return Err("Missing, invalid, or zero Content-Length header".into()); } @@ -86,7 +82,6 @@ async fn read_lsp_message( .into()); } - // Explicitly secure pre-allocation limit let mut buffer = vec![0u8; content_length]; reader.read_exact(&mut buffer).await?; @@ -94,7 +89,9 @@ async fn read_lsp_message( } impl RustAnalyzer { - pub fn new() -> Result> { + pub fn new( + notification_tx: tokio::sync::mpsc::UnboundedSender, + ) -> Result> { let mut child = tokio::process::Command::new("rust-analyzer") .stdin(Stdio::piped()) .stdout(Stdio::piped()) @@ -113,7 +110,6 @@ impl RustAnalyzer { 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); @@ -122,7 +118,6 @@ impl RustAnalyzer { Ok(v) => v, Err(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))); @@ -135,12 +130,14 @@ impl RustAnalyzer { Ok(v) => v, Err(err) => { eprintln!("Corrupted JSON received: {err}"); - continue; // Keep the connection running despite malformed frame + continue; } }; - // Filter server notification frames - if value.get("method").is_some() && value.get("id").is_none() { + let is_notification = value.get("method").is_some() && value.get("id").is_none(); + + if is_notification { + let _ = notification_tx.send(value); continue; } @@ -181,7 +178,6 @@ impl RustAnalyzer { let (tx, rx) = oneshot::channel(); - // Scope the lock allocation tightly { self.pending.lock().await.insert(id, tx); } @@ -193,19 +189,15 @@ impl RustAnalyzer { "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()), @@ -215,7 +207,6 @@ impl RustAnalyzer { ); } 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(), @@ -290,8 +281,68 @@ impl RustAnalyzer { } pub async fn initialized(&mut self) -> Result<(), Box> { - self.notify(Initialized::METHOD, InitializedParams {}) - .await?; + self.notify( + ::METHOD, + InitializedParams {}, + ) + .await?; Ok(()) } } + +impl RustAnalyzer { + pub async fn did_open( + &mut self, + uri: Url, + text: &str, + ) -> Result<(), Box> { + self.notify( + "textDocument/didOpen", + json!({ + "textDocument": { + "uri": uri, + "languageId": "rust", + "version": 1, + "text": text, + } + }), + ) + .await + } + + pub async fn did_change( + &mut self, + uri: Url, + text: &str, + version: i32, + ) -> Result<(), Box> { + self.notify( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": uri, + "version": version, + }, + "contentChanges": [{ + "text": text, + }], + }), + ) + .await + } + + pub async fn did_close( + &mut self, + uri: Url, + ) -> Result<(), Box> { + self.notify( + "textDocument/didClose", + json!({ + "textDocument": { + "uri": uri, + } + }), + ) + .await + } +} diff --git a/crates/analyzer/src/transpiler.rs b/crates/analyzer/src/transpiler.rs new file mode 100644 index 0000000..adf25fd --- /dev/null +++ b/crates/analyzer/src/transpiler.rs @@ -0,0 +1,41 @@ +use std::path::{Path, PathBuf}; + +use mist_codegen::{GetRust, RustCodegen}; +use mist_parser::rev_mapper::Mapping; +use mist_parser::{parse, parse_module}; + +pub struct TranspiledFile { + pub mist_path: PathBuf, + pub rust_path: PathBuf, + pub rust_content: String, + pub mapping: Mapping, +} + +pub fn transpile_mist(mist_path: &Path, source: &str) -> Result { + let module_decl = parse_module(source).map_err(|e| format!("parse module error: {e:?}"))?; + let rust_path = crate::from_mist_to_rust(mist_path.to_path_buf()); + + let parsed = parse(source).map_err(|e| format!("parse error: {e:?}"))?; + + for item in &parsed { + mist_parser::semantics::check_class_semantics(item) + .map_err(|e| format!("semantic error: {}", e[0].error_message))?; + } + + let mut codegen = RustCodegen::new(mist_path.to_path_buf()); + let output = codegen.generate(parsed); + + let mod_prefix = module_decl + .as_ref() + .map(|(vis, name)| format!("{}mod {};\n", vis.get_rust(), name.get_rust())) + .unwrap_or_default(); + + codegen.mapping.shift_rust(mod_prefix.lines().count() as isize, 0); + + Ok(TranspiledFile { + mist_path: mist_path.to_path_buf(), + rust_path, + rust_content: format!("{}{}", mod_prefix, output), + mapping: codegen.mapping, + }) +} diff --git a/crates/parser/src/rev_mapper.rs b/crates/parser/src/rev_mapper.rs index e943792..2b75e2f 100644 --- a/crates/parser/src/rev_mapper.rs +++ b/crates/parser/src/rev_mapper.rs @@ -34,6 +34,14 @@ impl Mapping { .max_by_key(|(rust, _)| *rust) } + pub fn find_by_mist(&self, target: &MistMap) -> Option<(RustMap, MistMap)> { + self.map + .iter() + .copied() + .filter(|(_, mist)| mist <= target) + .max_by_key(|(_, mist)| *mist) + } + pub fn shift_rust(&mut self, lines: isize, cols: isize) { self.map = self .map