diff --git a/Cargo.lock b/Cargo.lock index 17a2a50..df035ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,6 +448,8 @@ dependencies = [ "dashmap 6.1.0", "mist-codegen", "mist-parser", + "serde", + "serde_json", "tokio", "tower-lsp", ] diff --git a/crates/analyzer/Cargo.toml b/crates/analyzer/Cargo.toml index cc3c8c6..6274f4e 100644 --- a/crates/analyzer/Cargo.toml +++ b/crates/analyzer/Cargo.toml @@ -16,3 +16,6 @@ dashmap = "6.1.0" tokio = { version = "1.52.3", features = ["full", "macros", "rt", "rt-multi-thread"] } tower-lsp = "0.20.0" cargo_metadata = "0.23.1" + +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index fbbffe0..28e548d 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -1,23 +1,29 @@ pub mod builder; +pub mod rust_analyzer; pub mod transpiler; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fs; use std::path::{Component, PathBuf}; use std::sync::Arc; +use mist_parser::rev_mapper; use tokio::sync::Mutex; use tower_lsp::jsonrpc::Result; -use tower_lsp::lsp_types::*; +use tower_lsp::lsp_types::notification::Notification; +use tower_lsp::lsp_types::{self, *}; use tower_lsp::{Client, LanguageServer, LspService, Server}; use crate::builder::MistDiagnostic; +use crate::rust_analyzer::RustAnalyzer; #[derive(Debug)] struct Backend { client: Client, workspace_folder: Arc>>, previous_diagnostics: Arc>>>, + rust_analyzer: Arc>, + mapping: Arc>>>, } /// Helper function to force percent-encoding on Windows drive colons @@ -41,6 +47,44 @@ fn clean_lsp_url(path: &std::path::Path) -> Option { Url::parse(&url_str).ok() } +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, + }; + + match self.mapping.lock().await.get(&rs_path) { + Some(mapping) => { + // 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, + } + } + None => rs_loc, + } + } +} + #[tower_lsp::async_trait] impl LanguageServer for Backend { async fn initialize(&self, params: InitializeParams) -> Result { @@ -56,6 +100,12 @@ impl LanguageServer for Backend { }, )); + res.capabilities.completion_provider = Some(CompletionOptions { + resolve_provider: None, + trigger_characters: Some(vec![".".to_string()]), + ..Default::default() + }); + res.capabilities.definition_provider = Some(OneOf::Left(true)); let folder_path = params @@ -69,9 +119,19 @@ impl LanguageServer for Backend { } let workspace_folder = self.workspace_folder.clone(); + + let analyzer = self.rust_analyzer.clone(); + tokio::spawn(async move { if let Some(root) = &*workspace_folder.lock().await { transpiler::build(root); + + analyzer + .lock() + .await + .initialize(root) + .await + .expect("Failed to initialize rust analyzer"); } }); @@ -82,6 +142,13 @@ impl LanguageServer for Backend { self.client .log_message(MessageType::INFO, "server initialized!") .await; + + self.rust_analyzer + .lock() + .await + .initialized() + .await + .expect("Failed to initialize rust analyzer"); } async fn shutdown(&self) -> Result<()> { @@ -180,42 +247,160 @@ impl LanguageServer for Backend { *self.previous_diagnostics.lock().await = diagnostics; } + async fn did_open(&self, mut params: DidOpenTextDocumentParams) { + if params.text_document.language_id == "mist" { + params.text_document.language_id = "rust".to_string(); + + match transpiler::transpile_text(¶ms.text_document.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( + rust_path, + rev_mapper::get_mapping(¶ms.text_document.text), + ); + } + Err(e) => { + self.client + .log_message(MessageType::WARNING, format!("MIST-LSP: Syntax invalid during open/change. Parsing stopped: {:?}", e)) + .await; + return; + } + } + } + + if let Ok(mut ra) = self.rust_analyzer.try_lock() { + let _ = ra + .notify(notification::DidOpenTextDocument::METHOD, params) + .await; + } + } + async fn goto_definition( &self, params: GotoDefinitionParams, ) -> Result> { - let mut source = fs::read_to_string( - params - .text_document_position_params - .text_document - .uri - .to_file_path() - .unwrap(), - ) - .expect("Failed to read source"); + let file_path = params + .text_document_position_params + .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"; - - source = insert_at_position( + let injected_source = insert_at_position( &source, - params.text_document_position_params.position.line as usize, + params.text_document_position_params.position.line as usize + 1, params.text_document_position_params.position.character as usize, &inject, ); - let output = transpiler::transpile_text(&source).expect("Failed to transpile"); + let output = match transpiler::transpile_text(&injected_source) { + Ok(out) => out, + Err(_) => return Ok(None), + }; - let position = output - .find(inject) - .expect("Didn't find injection") - .saturating_sub(1); + 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"); + + let rs_res = self + .rust_analyzer + .lock() + .await + .request::(lsp_types::GotoDefinitionParams { + text_document_position_params: lsp_types::TextDocumentPositionParams { + position: lsp_types::Position { + line: line as u32 - 1, + character: character as u32, + }, + text_document: lsp_types::TextDocumentIdentifier { uri }, + }, + partial_result_params: lsp_types::PartialResultParams::default(), + work_done_progress_params: lsp_types::WorkDoneProgressParams::default(), + }) + .await + .expect("Failed to send to rust"); + + Ok(match rs_res { + Some(GotoDefinitionResponse::Array(arr)) => { + let mut mapped_arr = Vec::new(); + for rs_loc in arr { + let mapped_loc = self.get_mist_location(rs_loc).await; + mapped_arr.push(mapped_loc); + } + Some(GotoDefinitionResponse::Array(mapped_arr)) + } + _ => rs_res, + }) + } + + async fn completion(&self, _params: CompletionParams) -> Result> { self.client - .log_message(MessageType::INFO, format!("Found at {position}")) + .log_message(MessageType::INFO, "getting completion") .await; - Ok(None) + Ok(Some(CompletionResponse::Array(vec![ + CompletionItem::new_simple("new".to_string(), "The new keyword".to_string()), + ]))) } + + // 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] @@ -227,6 +412,10 @@ pub async fn start() { client, workspace_folder: Arc::new(Mutex::new(None)), previous_diagnostics: Arc::new(Mutex::new(HashMap::new())), + mapping: Arc::new(Mutex::new(HashMap::new())), + rust_analyzer: Arc::new(Mutex::new( + RustAnalyzer::new().expect("Failed to create rust analyzer"), + )), }); Server::new(stdin, stdout, socket).serve(service).await; } @@ -245,6 +434,34 @@ pub fn from_mist_to_rust(mut path: PathBuf) -> PathBuf { } } +pub fn from_rust_to_mist(mut path: PathBuf) -> PathBuf { + // reverse extension + path.set_extension("mist"); + + let comps: Vec = path.components().collect(); + + // pattern we originally inserted: ".mist/lsp" + let pattern: Vec = std::path::Path::new(".mist/lsp").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 + } +} + 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(); @@ -270,3 +487,20 @@ fn insert_at_position(s: &str, line: usize, col: usize, insert: &str) -> String lines.join("\n") } + +fn find_row_col(output: &str, inject: &str) -> Option<(usize, usize)> { + // 1. Find the flat byte index just like your original code + let byte_idx = output.find(inject)?; + + // 2. Slice the string up to the match point + let prefix = &output[..byte_idx]; + + // 3. Row = number of newlines found before the match + 1 (1-indexed) + let row = prefix.lines().count(); + + // 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; + + Some((row, col)) +} diff --git a/crates/analyzer/src/rust_analyzer.rs b/crates/analyzer/src/rust_analyzer.rs new file mode 100644 index 0000000..e2cc377 --- /dev/null +++ b/crates/analyzer/src/rust_analyzer.rs @@ -0,0 +1,237 @@ +use std::{collections::HashMap, path::PathBuf, process::Stdio, sync::Arc}; + +use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use serde_json::{Value, json}; +use tokio::{ + io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}, + sync::{Mutex, oneshot}, +}; +use tower_lsp::lsp_types::{ + self, ClientCapabilities, InitializeParams, InitializedParams, Url, WorkspaceFolder, + notification::{Initialized, Notification}, + request::{self, Request}, +}; + +#[derive(Debug, Deserialize)] +pub struct JsonRpcResponse { + pub jsonrpc: String, + pub id: Option, + pub result: Option, + pub error: Option, +} + +#[derive(Debug, Deserialize)] +pub struct JsonRpcError { + pub code: i64, + pub message: String, + pub data: Option, +} + +type PendingMap = Arc>>>; + +#[derive(Debug)] +pub struct RustAnalyzer { + stdin: tokio::process::ChildStdin, + pending: PendingMap, + id: usize, +} + +async fn send_lsp_message( + writer: &mut W, + value: &serde_json::Value, +) -> std::io::Result<()> { + let payload = serde_json::to_string(value)?; + let frame = format!("Content-Length: {}\r\n\r\n{}", payload.len(), payload); + writer.write_all(frame.as_bytes()).await?; + writer.flush().await?; + Ok(()) +} + +async fn read_lsp_message( + reader: &mut R, +) -> Result> { + let mut line = String::new(); + let mut content_length = 0; + + // Read headers until we hit the empty separator line (\r\n) + loop { + line.clear(); + reader.read_line(&mut line).await?; + if line == "\r\n" || line.is_empty() { + break; + } + if line.to_lowercase().starts_with("content-length:") { + content_length = line["content-length:".len()..].trim().parse::()?; + } + } + + if content_length == 0 { + return Err("Missing or invalid Content-Length header".into()); + } + + // Read the exact byte buffer payload + let mut buffer = vec![0u8; content_length]; + reader.read_exact(&mut buffer).await?; + + Ok(String::from_utf8(buffer)?) +} + +impl RustAnalyzer { + 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 + .spawn()?; + + let stdin = child.stdin.take().unwrap(); + let stdout = child.stdout.take().unwrap(); + + let pending: PendingMap = Arc::new(Mutex::new(HashMap::new())); + let pending_clone = pending.clone(); + + tokio::spawn(async move { + let mut stdout = BufReader::new(stdout); + + loop { + let raw = match read_lsp_message(&mut stdout).await { + Ok(v) => v, + Err(err) => { + eprintln!("LSP read error: {err}"); + break; + } + }; + + let value: Value = match serde_json::from_str(&raw) { + Ok(v) => v, + Err(err) => { + eprintln!("Invalid JSON from rust-analyzer: {err}"); + continue; + } + }; + + if value.get("method").is_some() { + 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}"); + } + } + } + }); + + Ok(Self { + stdin, + pending, + id: 0, + }) + } + + pub async fn request( + &mut self, + params: R::Params, + ) -> Result> + where + R::Result: DeserializeOwned, + { + let id = { + self.id += 1; + self.id + }; + + 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)?; + + if let Some(err) = envelope.error { + return Err(format!("LSP Error ({}): {}", err.code, err.message).into()); + } + + envelope.result.ok_or_else(|| "missing result".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 + } +} + +impl RustAnalyzer { + 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")?; + + #[allow(deprecated)] + let init_params = InitializeParams { + process_id: Some(std::process::id()), + root_uri: Some(project_uri.clone()), + workspace_folders: Some(vec![WorkspaceFolder { + uri: project_uri, + name: "workspace".to_string(), + }]), + capabilities: ClientCapabilities { + workspace: Some(lsp_types::WorkspaceClientCapabilities { + workspace_folders: Some(true), + ..Default::default() + }), + text_document: Some(lsp_types::TextDocumentClientCapabilities { + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }; + + self.request::(init_params).await?; + + Ok(()) + } + + pub async fn initialized(&mut self) -> Result<(), Box> { + self.notify(Initialized::METHOD, InitializedParams {}) + .await?; + + Ok(()) + } +} diff --git a/src/builder.rs b/src/builder.rs index dfe510b..ec74fc6 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -79,7 +79,11 @@ pub fn build(mut args: Vec, root: PathBuf) -> bool { .expect("Unable to find mapping"); let mist_msg = MistDiagnosticMessage { - message: span.label.clone().unwrap_or(msg.message.message.clone()), + message: format!( + "{}: {}", + msg.message.message, + span.label.clone().unwrap_or_default() + ), file_name: if is_root { mist_file } else {