From 15466604357494eb8b70b8f7358b0773c5ccc795 Mon Sep 17 00:00:00 2001 From: Selimaj Dev Date: Sun, 21 Jun 2026 22:52:49 +0200 Subject: [PATCH 01/13] Fixed initialization error --- analyzer.bat | 2 +- crates/analyzer/src/rust_analyzer.rs | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/analyzer.bat b/analyzer.bat index 8ffbbf8..446d793 100644 --- a/analyzer.bat +++ b/analyzer.bat @@ -4,6 +4,6 @@ cd D:\projects\mist\mist mist build >nul 2>&1 -cd D:\projects\mist\mist-lsp-test +cd mist-test D:\projects\mist\mist\target\debug\mist-analyzer.exe diff --git a/crates/analyzer/src/rust_analyzer.rs b/crates/analyzer/src/rust_analyzer.rs index 2fbee2f..8a4a7d3 100644 --- a/crates/analyzer/src/rust_analyzer.rs +++ b/crates/analyzer/src/rust_analyzer.rs @@ -57,11 +57,16 @@ async fn read_lsp_message( ) -> Result> { let mut line = String::new(); let mut content_length = 0; + let mut eof = false; for _ in 0..100 { line.clear(); let bytes_read = reader.read_line(&mut line).await?; - if bytes_read == 0 || line.trim().is_empty() { + if bytes_read == 0 { + eof = true; + break; + } + if line.trim().is_empty() { break; } if line.to_ascii_lowercase().starts_with("content-length:") { @@ -75,6 +80,9 @@ async fn read_lsp_message( } if content_length == 0 { + if eof { + return Err("LSP stream closed (EOF)".into()); + } return Err("Missing, invalid, or zero Content-Length header".into()); } if content_length > MAX_CONTENT_LENGTH { @@ -126,8 +134,11 @@ impl RustAnalyzer { for (_, tx) in lock.drain() { let _ = tx.send(Err(format!("LSP reader error: {}", err))); } - // Brief backoff then retry — ra may have sent a malformed frame - // but the connection is still alive. + // EOF means the child process exited — stop the reader. + // Other errors (malformed frames) get a brief backoff then retry. + if err.to_string().contains("LSP stream closed (EOF)") { + break; + } tokio::time::sleep(Duration::from_millis(50)).await; continue; } From 6a7c45ffbe1efdd3c71284ff961b18366ac94a58 Mon Sep 17 00:00:00 2001 From: Selimaj Dev Date: Sun, 21 Jun 2026 23:08:53 +0200 Subject: [PATCH 02/13] Debugging --- crates/analyzer/src/lib.rs | 32 ++++++++++++++++++++--- crates/analyzer/src/rust_analyzer.rs | 38 +++++++++++++++++++++++++--- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index 65a74ec..a6daff3 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -157,9 +157,32 @@ impl Backend { modified.push(' '); modified.push_str(&source[offset..]); - let transpiled = transpile_mist(mist_path, &modified, extra_mod_decl).ok()?; - let rust_pos = find_marker_position(&transpiled.rust_content, &marker)?; - let rust_uri = clean_lsp_url(&transpiled.rust_path)?; + let transpiled = match transpile_mist(mist_path, &modified, extra_mod_decl) { + Ok(t) => t, + Err(e) => { + eprintln!("[marker] transpile failed for {marker} at LSP({line},{character}): {e}"); + return None; + } + }; + let rust_pos = match find_marker_position(&transpiled.rust_content, &marker) { + Some(p) => p, + None => { + eprintln!("[marker] marker {marker} not found in transpiled output"); + return None; + } + }; + let rust_uri = match clean_lsp_url(&transpiled.rust_path) { + Some(u) => u, + None => { + eprintln!("[marker] failed to create URI for {:?}", transpiled.rust_path); + return None; + } + }; + + eprintln!( + "[marker] {marker}: mist LSP({line},{character}) -> rust LSP({},{})", + rust_pos.line, rust_pos.character + ); Some((rust_uri, rust_pos)) } @@ -552,6 +575,7 @@ impl LanguageServer for Backend { ) .await else { + eprintln!("[completion] resolve_mist_via_marker returned None at ({}, {})", mist_pos.line, mist_pos.character); return Ok(None); }; @@ -624,6 +648,7 @@ impl LanguageServer for Backend { ) .await else { + eprintln!("[goto_definition] resolve_mist_via_marker returned None at ({}, {})", mist_pos.line, mist_pos.character); return Ok(None); }; @@ -747,6 +772,7 @@ impl LanguageServer for Backend { ) .await else { + eprintln!("[hover] resolve_mist_via_marker returned None at ({}, {})", mist_pos.line, mist_pos.character); return Ok(None); }; diff --git a/crates/analyzer/src/rust_analyzer.rs b/crates/analyzer/src/rust_analyzer.rs index 8a4a7d3..a5ba763 100644 --- a/crates/analyzer/src/rust_analyzer.rs +++ b/crates/analyzer/src/rust_analyzer.rs @@ -47,8 +47,16 @@ async fn send_lsp_message( ) -> 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?; + eprintln!("[send] writing {} bytes to child stdin", frame.len()); + if let Err(e) = writer.write_all(frame.as_bytes()).await { + eprintln!("[send] write_all failed: {e}"); + return Err(e); + } + if let Err(e) = writer.flush().await { + eprintln!("[send] flush failed: {e}"); + return Err(e); + } + eprintln!("[send] write OK"); Ok(()) } @@ -106,7 +114,7 @@ impl RustAnalyzer { let mut child = tokio::process::Command::new("rust-analyzer") .stdin(Stdio::piped()) .stdout(Stdio::piped()) - .stderr(Stdio::null()) + .stderr(Stdio::piped()) .spawn()?; let stdin = child @@ -117,12 +125,35 @@ impl RustAnalyzer { .stdout .take() .ok_or("Failed to open child stdout pipe")?; + let stderr = child + .stderr + .take() + .ok_or("Failed to open child stderr pipe")?; + + tokio::spawn(async move { + let mut stderr = stderr; + let mut buf = vec![0u8; 4096]; + loop { + match stderr.read(&mut buf).await { + Ok(0) => break, + Ok(n) => { + eprintln!("[ra-stderr] {}", String::from_utf8_lossy(&buf[..n]).trim_end()); + } + Err(e) => { + eprintln!("[ra-stderr] read error: {e}"); + break; + } + } + } + eprintln!("[ra-stderr] stream closed"); + }); let pending: PendingMap = Arc::new(Mutex::new(HashMap::new())); let pending_clone = pending.clone(); tokio::spawn(async move { let mut stdout = BufReader::new(stdout); + eprintln!("[ra-reader] started"); loop { let raw = match read_lsp_message(&mut stdout).await { @@ -137,6 +168,7 @@ impl RustAnalyzer { // EOF means the child process exited — stop the reader. // Other errors (malformed frames) get a brief backoff then retry. if err.to_string().contains("LSP stream closed (EOF)") { + eprintln!("LSP reader task: EOF detected, stopping reader"); break; } tokio::time::sleep(Duration::from_millis(50)).await; From 5054185e0d3e578d71fcee552bf32f259857c695 Mon Sep 17 00:00:00 2001 From: Selimaj Dev Date: Sun, 21 Jun 2026 23:14:07 +0200 Subject: [PATCH 03/13] Fixed bugs --- crates/analyzer/src/lib.rs | 34 +++++++++++++++++++++------- crates/analyzer/src/rust_analyzer.rs | 12 ++-------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index a6daff3..87ceec7 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -80,6 +80,18 @@ fn byte_offset_from_lsp(source: &str, line: u32, character: u32) -> Option usize { + let before = &source[..offset]; + let trim = before + .chars() + .rev() + .take_while(|c| c.is_alphanumeric() || *c == '_') + .count(); + offset - trim +} + fn find_marker_position(content: &str, marker: &str) -> Option { let idx = content.find(marker)?; let before = &content[..idx]; @@ -149,6 +161,7 @@ impl Backend { let marker = format!("__mist_mk{id:x}__"); let offset = byte_offset_from_lsp(source, line, character)?; + let offset = snap_to_word_start(source, offset); let mut modified = String::with_capacity(source.len() + marker.len() + 4); modified.push_str(&source[..offset]); @@ -373,14 +386,19 @@ impl LanguageServer for Backend { if let Some(root) = &*ws.lock().await { let src_root = root.join("src"); - 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; + // Hold the lock across both initialize + initialized so the + // editor's didOpen cannot race in-between and send notifications + // to rust-analyzer before it has received Initialized. + { + let mut guard = ra.lock().await; + if let Err(e) = guard.initialize(root).await { + eprintln!("Failed to initialize rust-analyzer: {e}"); + return; + } + if let Err(e) = guard.initialized().await { + eprintln!("rust-analyzer initialized failed: {e}"); + return; + } } let mut files = Vec::new(); diff --git a/crates/analyzer/src/rust_analyzer.rs b/crates/analyzer/src/rust_analyzer.rs index a5ba763..b75d717 100644 --- a/crates/analyzer/src/rust_analyzer.rs +++ b/crates/analyzer/src/rust_analyzer.rs @@ -47,16 +47,8 @@ async fn send_lsp_message( ) -> std::io::Result<()> { let payload = serde_json::to_string(value)?; let frame = format!("Content-Length: {}\r\n\r\n{}", payload.len(), payload); - eprintln!("[send] writing {} bytes to child stdin", frame.len()); - if let Err(e) = writer.write_all(frame.as_bytes()).await { - eprintln!("[send] write_all failed: {e}"); - return Err(e); - } - if let Err(e) = writer.flush().await { - eprintln!("[send] flush failed: {e}"); - return Err(e); - } - eprintln!("[send] write OK"); + writer.write_all(frame.as_bytes()).await?; + writer.flush().await?; Ok(()) } From 173c983c6f3a84edd5ad8a68c9628845c4665f0a Mon Sep 17 00:00:00 2001 From: Selimaj Dev Date: Sun, 21 Jun 2026 23:37:50 +0200 Subject: [PATCH 04/13] Working markers --- crates/analyzer/src/lib.rs | 60 ++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index 87ceec7..cece78d 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -80,16 +80,48 @@ fn byte_offset_from_lsp(source: &str, line: u32, character: u32) -> Option usize { +/// Inject a marker at the exact cursor position. If the cursor is inside or +/// at the start of an identifier-like token, the entire token is replaced with +/// the marker so parsing doesn't break from a mid-token split. +fn inject_marker_at(source: &str, line: u32, character: u32, marker: &str) -> Option { + let offset = byte_offset_from_lsp(source, line, character)?; + let before = &source[..offset]; - let trim = before + let after = &source[offset..]; + + // Identifier chars immediately before and after the cursor (as byte-counts). + let trail_bytes: usize = before .chars() .rev() .take_while(|c| c.is_alphanumeric() || *c == '_') - .count(); - offset - trim + .map(|c| c.len_utf8()) + .sum(); + let lead_bytes: usize = after + .chars() + .take_while(|c| c.is_alphanumeric() || *c == '_') + .map(|c| c.len_utf8()) + .sum(); + + if trail_bytes > 0 || lead_bytes > 0 { + // Cursor touches an identifier — replace the whole token with the marker. + let token_start = offset - trail_bytes; + let token_end = offset + lead_bytes; + + let mut s = String::with_capacity(source.len() + marker.len() + 4); + s.push_str(&source[..token_start]); + s.push_str(marker); + s.push_str(&source[token_end..]); + Some(s) + } else { + // Cursor is on whitespace / punctuation — inject as standalone expression. + let mut s = String::with_capacity(source.len() + marker.len() + 4); + s.push_str(&source[..offset]); + s.push(' '); + s.push_str(marker); + s.push(' '); + s.push_str(&source[offset..]); + Some(s) + } } fn find_marker_position(content: &str, marker: &str) -> Option { @@ -160,15 +192,13 @@ impl Backend { let id = MARKER_COUNTER.fetch_add(1, Ordering::Relaxed); let marker = format!("__mist_mk{id:x}__"); - let offset = byte_offset_from_lsp(source, line, character)?; - let offset = snap_to_word_start(source, offset); - - let mut modified = String::with_capacity(source.len() + marker.len() + 4); - modified.push_str(&source[..offset]); - modified.push(' '); - modified.push_str(&marker); - modified.push(' '); - modified.push_str(&source[offset..]); + let modified = match inject_marker_at(source, line, character, &marker) { + Some(m) => m, + None => { + eprintln!("[marker] inject_marker_at returned None for LSP({line},{character})"); + return None; + } + }; let transpiled = match transpile_mist(mist_path, &modified, extra_mod_decl) { Ok(t) => t, From c3f56dd3e98e83cb71ace241a5047f15c1fb7c5c Mon Sep 17 00:00:00 2001 From: Selimaj Dev Date: Mon, 22 Jun 2026 00:01:24 +0200 Subject: [PATCH 05/13] Fixed punctuation error --- crates/analyzer/src/lib.rs | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index cece78d..0aa7c52 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -111,19 +111,32 @@ fn inject_marker_at(source: &str, line: u32, character: u32, marker: &str) -> Op s.push_str(&source[..token_start]); s.push_str(marker); s.push_str(&source[token_end..]); - Some(s) - } else { - // Cursor is on whitespace / punctuation — inject as standalone expression. - let mut s = String::with_capacity(source.len() + marker.len() + 4); - s.push_str(&source[..offset]); - s.push(' '); - s.push_str(marker); - s.push(' '); - s.push_str(&source[offset..]); - Some(s) + return Some(s); } + + // Not on an identifier. If the cursor is on a `.` treat it as a field-access + // and inject the marker as an identifier right after the dot. + if after.starts_with('.') { + let dot_end = offset + '.'. + len_utf8(); + let mut s = String::with_capacity(source.len() + marker.len() + 4); + s.push_str(&source[..dot_end]); + s.push_str(marker); + s.push_str(&source[dot_end..]); + return Some(s); + } + + // Whitespace / other punctuation — inject as standalone expression. + let mut s = String::with_capacity(source.len() + marker.len() + 4); + s.push_str(&source[..offset]); + s.push(' '); + s.push_str(marker); + s.push(' '); + s.push_str(&source[offset..]); + Some(s) } + fn find_marker_position(content: &str, marker: &str) -> Option { let idx = content.find(marker)?; let before = &content[..idx]; From 5c6b3bbca174f9533344bfebb90fb730e91e96ec Mon Sep 17 00:00:00 2001 From: Selimaj Dev Date: Mon, 22 Jun 2026 00:09:27 +0200 Subject: [PATCH 06/13] Fixed punctuation --- crates/analyzer/src/lib.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index 0aa7c52..0a562d0 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -240,6 +240,19 @@ impl Backend { rust_pos.line, rust_pos.character ); + // If actual source can't be transpiled, rust-analyzer's file is stale. + // Push the markered transpiled content so the upcoming LSP request (completion, + // hover, goto_def) sees the right context (the marker acts as placeholder). + if transpile_mist(mist_path, source, extra_mod_decl).is_err() { + let mut ra = self.rust_analyzer.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 _ = ra + .did_change(rust_uri.clone(), &transpiled.rust_content, *version) + .await; + } + Some((rust_uri, rust_pos)) } From 48c24235b14cc7baf5d97ba598f7ae12ee2f47ae Mon Sep 17 00:00:00 2001 From: Selimaj Dev Date: Mon, 22 Jun 2026 00:19:42 +0200 Subject: [PATCH 07/13] Goto works (kind of) --- crates/analyzer/src/lib.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index 0a562d0..5529df0 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -701,6 +701,10 @@ impl LanguageServer for Backend { ) -> tower_lsp::jsonrpc::Result> { let mist_uri = params.text_document_position_params.text_document.uri; let mist_pos = params.text_document_position_params.position; + eprintln!( + "[goto_definition] entered at mist ({}, {})", + mist_pos.line, mist_pos.character + ); let mist_path = match mist_uri.to_file_path() { Ok(p) => p, @@ -728,7 +732,7 @@ impl LanguageServer for Backend { let gd_params = GotoDefinitionParams { text_document_position_params: TextDocumentPositionParams { - text_document: TextDocumentIdentifier { uri: rust_uri }, + text_document: TextDocumentIdentifier { uri: rust_uri.clone() }, position: rust_pos, }, work_done_progress_params: WorkDoneProgressParams { @@ -738,6 +742,10 @@ impl LanguageServer for Backend { partial_result_token: None, }, }; + eprintln!( + "[goto_definition] rust-analyzer request at {} ({},{})", + rust_uri, rust_pos.line, rust_pos.character + ); match self .rust_analyzer @@ -747,6 +755,7 @@ impl LanguageServer for Backend { .await { Ok(Some(GotoDefinitionResponse::Scalar(loc))) => { + eprintln!("[goto_definition] Scalar response"); let mapped = self.map_rust_to_mist_pos(&loc.uri, &loc.range.start).await; match mapped { Some((mist_uri, mist_start)) => { @@ -762,10 +771,14 @@ impl LanguageServer for Backend { range: mist_range, }))) } - None => Ok(Some(GotoDefinitionResponse::Scalar(loc))), + None => { + eprintln!("[goto_definition] map_rust_to_mist_pos failed, returning raw rust loc"); + Ok(Some(GotoDefinitionResponse::Scalar(loc))) + } } } Ok(Some(GotoDefinitionResponse::Array(locs))) => { + eprintln!("[goto_definition] Array response with {} items", locs.len()); let mut mapped = Vec::new(); for loc in locs { if let Some((mist_uri, mist_start)) = @@ -786,6 +799,7 @@ impl LanguageServer for Backend { Ok(Some(GotoDefinitionResponse::Array(mapped))) } Ok(Some(GotoDefinitionResponse::Link(links))) => { + eprintln!("[goto_definition] Link response with {} items", links.len()); let mut mapped = Vec::new(); for link in links { if let Some((mist_uri, mist_start)) = self @@ -814,7 +828,10 @@ impl LanguageServer for Backend { } Ok(Some(GotoDefinitionResponse::Link(mapped))) } - Ok(None) => Ok(None), + Ok(None) => { + eprintln!("[goto_definition] rust-analyzer returned None"); + Ok(None) + } Err(e) => { eprintln!("goto_definition error: {e}"); Ok(None) From b69168f1352ef41030ebe95356f1907fbde3c1d9 Mon Sep 17 00:00:00 2001 From: Selimaj Dev Date: Mon, 22 Jun 2026 00:23:50 +0200 Subject: [PATCH 08/13] Fixed native rust goto --- crates/analyzer/src/lib.rs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index 5529df0..31c218f 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -756,8 +756,7 @@ impl LanguageServer for Backend { { Ok(Some(GotoDefinitionResponse::Scalar(loc))) => { eprintln!("[goto_definition] Scalar response"); - let mapped = self.map_rust_to_mist_pos(&loc.uri, &loc.range.start).await; - match mapped { + match self.map_rust_to_mist_pos(&loc.uri, &loc.range.start).await { Some((mist_uri, mist_start)) => { let mist_range = Range { start: mist_start, @@ -772,8 +771,15 @@ impl LanguageServer for Backend { }))) } None => { - eprintln!("[goto_definition] map_rust_to_mist_pos failed, returning raw rust loc"); - Ok(Some(GotoDefinitionResponse::Scalar(loc))) + if let Some(normalized) = loc.uri.to_file_path().ok().and_then(|p| clean_lsp_url(&p)) { + eprintln!("[goto_definition] returning raw rust loc"); + Ok(Some(GotoDefinitionResponse::Scalar(Location { + uri: normalized, + range: loc.range, + }))) + } else { + Ok(Some(GotoDefinitionResponse::Scalar(loc))) + } } } } @@ -794,6 +800,11 @@ impl LanguageServer for Backend { }, }, }); + } else if let Some(normalized) = loc.uri.to_file_path().ok().and_then(|p| clean_lsp_url(&p)) { + mapped.push(Location { + uri: normalized, + range: loc.range, + }); } } Ok(Some(GotoDefinitionResponse::Array(mapped))) @@ -824,6 +835,15 @@ impl LanguageServer for Backend { }, }, }); + } else if let Some(normalized) = + link.target_uri.to_file_path().ok().and_then(|p| clean_lsp_url(&p)) + { + mapped.push(LocationLink { + origin_selection_range: link.origin_selection_range, + target_uri: normalized, + target_range: link.target_range, + target_selection_range: link.target_selection_range, + }); } } Ok(Some(GotoDefinitionResponse::Link(mapped))) From 2718a5028a3b450f530031e4b4363cebd94b967e Mon Sep 17 00:00:00 2001 From: Selimaj Dev Date: Mon, 22 Jun 2026 00:48:13 +0200 Subject: [PATCH 09/13] Fixed module system --- crates/analyzer/src/lib.rs | 112 +++++++++++++++++++++++++++++- crates/analyzer/src/transpiler.rs | 24 +++---- 2 files changed, 122 insertions(+), 14 deletions(-) diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index 31c218f..1777d9e 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -346,6 +346,8 @@ impl Backend { let src_root = ws.join("src"); let package_mist = read_mist_package(&ws); + self.ensure_implicit_packages(&src_root).await; + // Collect sources first to avoid holding locks during transpile let sources: Vec<(PathBuf, String, String)> = { let docs = self.documents.lock().await; @@ -366,6 +368,13 @@ impl Backend { } } + /// Create synthetic `package.mist` documents for subdirectories that contain + /// .mist files but no real package.mist (implicit packages). + async fn ensure_implicit_packages(&self, src_root: &Path) { + let mut docs = self.documents.lock().await; + ensure_implicit_packages_impl(&mut *docs, src_root); + } + async fn publish_diagnostics(&self, uri: Url, diagnostics: Vec) { let key = uri.clone(); let prev = self.previous_diagnostics.lock().await.get(&key).cloned(); @@ -471,6 +480,8 @@ impl LanguageServer for Backend { eprintln!("Loaded {} mist files", files.len()); + ensure_implicit_packages_impl(&mut *documents.lock().await, &src_root); + // Compute mod_decls before any transpile so the root file gets its // child module declarations from the very first didOpen. let package_mist = read_mist_package(root); @@ -510,6 +521,41 @@ impl LanguageServer for Backend { } } + // Also transpile synthetic package.mist files (implicit packages) + // so rust-analyzer has their content from the start. + let synthetic_paths: Vec = documents + .lock() + .await + .keys() + .filter(|p| { + !files.contains(p) + && p.file_name().and_then(|n| n.to_str()) == Some("package.mist") + }) + .cloned() + .collect(); + for syn_path in &synthetic_paths { + let decl = mod_decls.get(syn_path).map(String::as_str).unwrap_or(""); + let source = documents + .lock() + .await + .get(syn_path) + .map(|r| r.to_string()) + .unwrap_or_default(); + if let Ok(transpiled) = transpile_mist(syn_path, &source, decl) { + if let Some(rust_uri) = clean_lsp_url(&transpiled.rust_path) { + let _ = ra + .lock() + .await + .did_open(rust_uri, &transpiled.rust_content) + .await; + mapping + .lock() + .await + .insert(transpiled.rust_path, transpiled.mapping); + } + } + } + if let Some(rx) = notification_rx.lock().await.take() { tokio::spawn(handle_ra_notifications( rx, @@ -1099,10 +1145,15 @@ fn compute_mod_decls( Some(s) => s, None => continue, }; - // Skip the root package file itself + // Skip the root package entry file (e.g. main.mist) if *file == package_path { continue; } + // Skip any subdirectory package.mist — it's the directory's own + // entry file, not a sibling submodule. + if stem == "package" { + continue; + } mod_decl.push_str(&format!("pub mod {};\n", stem)); } @@ -1118,9 +1169,68 @@ fn compute_mod_decls( } } + // For each subdirectory that is a Mist module, add pub mod ; + // to the parent directory's declarations so the parent's transpiled Rust + // output includes the child module declaration. + for dir in dirs.keys() { + if dir == src_root { + continue; + } + let dirname = match dir.file_name().and_then(|n| n.to_str()) { + Some(n) => n, + None => continue, + }; + let parent = dir.parent().unwrap(); + let parent_target = if parent == src_root { + package_path.clone() + } else { + let pkg = parent.join("package.mist"); + if !documents.contains_key(&pkg) { + continue; + } + pkg + }; + result + .entry(parent_target) + .or_default() + .push_str(&format!("pub mod {};\n", dirname)); + } + result } +fn ensure_implicit_packages_impl( + docs: &mut HashMap, + src_root: &Path, +) { + let mut dirs_with_mist: HashMap> = HashMap::new(); + for mist_path in docs.keys() { + if let Ok(rel) = mist_path.strip_prefix(src_root) { + if let Some(parent) = rel.parent() { + if !parent.as_os_str().is_empty() { + dirs_with_mist + .entry(src_root.join(parent)) + .or_default() + .push(mist_path.clone()); + } + } + } + } + + for dir in dirs_with_mist.keys() { + let pkg_path = dir.join("package.mist"); + if !docs.contains_key(&pkg_path) { + let dirname = dir + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("module") + .to_string(); + let content = format!("pub module {dirname};\n"); + docs.insert(pkg_path, Rope::from_str(&content)); + } + } +} + fn clean_completion_item(mut item: CompletionItem) -> CompletionItem { item.text_edit = None; item.additional_text_edits = None; diff --git a/crates/analyzer/src/transpiler.rs b/crates/analyzer/src/transpiler.rs index 763674c..581ae12 100644 --- a/crates/analyzer/src/transpiler.rs +++ b/crates/analyzer/src/transpiler.rs @@ -1,8 +1,8 @@ use std::path::{Path, PathBuf}; -use mist_codegen::{GetRust, RustCodegen}; +use mist_codegen::RustCodegen; use mist_parser::rev_mapper::Mapping; -use mist_parser::{parse, parse_module}; +use mist_parser::parse; pub struct TranspiledFile { pub mist_path: PathBuf, @@ -16,8 +16,13 @@ pub fn transpile_mist( source: &str, extra_mod_decl: &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 mut rust_path = crate::from_mist_to_rust(mist_path.to_path_buf()); + // Package files (package.mist) must output as /mod.rs so the Rust module + // hierarchy resolves correctly (pub mod ; declarations look for sibling + // .rs files, and the parent module declaration looks for /mod.rs). + if mist_path.file_name().and_then(|n| n.to_str()) == Some("package.mist") { + rust_path.set_file_name("mod.rs"); + } let parsed = parse(source).map_err(|e| format!("parse error: {e:?}"))?; @@ -29,19 +34,12 @@ pub fn transpile_mist( let mut codegen = RustCodegen::new(mist_path.to_path_buf()); let output = codegen.generate(parsed); - let self_mod_prefix = module_decl - .as_ref() - .map(|(vis, name)| format!("{}mod {};\n", vis.get_rust(), name.get_rust())) - .unwrap_or_default(); - - let combined_prefix = format!("{}{}", extra_mod_decl, self_mod_prefix); - - codegen.mapping.shift_rust(combined_prefix.lines().count() as isize, 0); + codegen.mapping.shift_rust(extra_mod_decl.lines().count() as isize, 0); Ok(TranspiledFile { mist_path: mist_path.to_path_buf(), rust_path, - rust_content: format!("{}{}", combined_prefix, output), + rust_content: format!("{}{}", extra_mod_decl, output), mapping: codegen.mapping, }) } From cb626757bad12d39b7f316b214519c3c80a58635 Mon Sep 17 00:00:00 2001 From: Selimaj Dev Date: Mon, 22 Jun 2026 00:59:30 +0200 Subject: [PATCH 10/13] completion syntax exceptions --- crates/analyzer/src/lib.rs | 75 +++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index 1777d9e..2370c42 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -28,6 +28,7 @@ struct Backend { mapping: Arc>>, documents: Arc>>, doc_versions: Arc>>, + last_rust_contents: Arc>>, notification_rx: Arc>>>, } @@ -217,7 +218,65 @@ impl Backend { Ok(t) => t, Err(e) => { eprintln!("[marker] transpile failed for {marker} at LSP({line},{character}): {e}"); - return None; + + // Fallback: the source has a syntax error that prevents transpilation. + // Use the last known working Rust content + mapping to inject the + // marker at the nearest valid Rust position. + let mist_target = MistMap(line as usize + 1, character as usize); + let mut rust_path = crate::from_mist_to_rust(mist_path.to_path_buf()); + if mist_path.file_name().and_then(|n| n.to_str()) == Some("package.mist") { + rust_path.set_file_name("mod.rs"); + } + + let (last_mapping, last_content) = { + let map_guard = self.mapping.lock().await; + let content_guard = self.last_rust_contents.lock().await; + let m = map_guard.get(&rust_path)?.clone(); + let c = content_guard.get(mist_path)?.clone(); + (m, c) + }; + + let (_rust_before, mist_before) = last_mapping.find_by_mist(&mist_target)?; + + // Find the next mapping entry after mist_before — the marker goes + // right before it in the Rust output, filling the gap. + let rust_next = last_mapping + .map + .iter() + .filter(|(_, mist)| *mist > mist_before) + .min_by_key(|(_, mist)| *mist) + .map(|(rust, _)| *rust); + + let (modified_rust, rust_pos) = if let Some(next_rust) = rust_next { + let lsp_line = (next_rust.0 - 1) as u32; + let lsp_col = next_rust.1 as u32; + let modified = inject_marker_at(&last_content, lsp_line, lsp_col, &marker)?; + let pos = find_marker_position(&modified, &marker)?; + (modified, pos) + } else { + let modified = format!("{last_content}\nlet _ = {};", marker); + let pos = find_marker_position(&modified, &marker)?; + (modified, pos) + }; + + let rust_uri = clean_lsp_url(&rust_path)?; + + { + let mut ra = self.rust_analyzer.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 _ = ra + .did_change(rust_uri.clone(), &modified_rust, *version) + .await; + } + + eprintln!( + "[marker] {marker} (fallback): mist LSP({line},{character}) -> rust LSP({},{})", + rust_pos.line, rust_pos.character + ); + + return Some((rust_uri, rust_pos)); } }; let rust_pos = match find_marker_position(&transpiled.rust_content, &marker) { @@ -335,6 +394,10 @@ impl Backend { } map.insert(transpiled.rust_path, transpiled.mapping); + self.last_rust_contents + .lock() + .await + .insert(mist_path.to_path_buf(), transpiled.rust_content); } } @@ -443,6 +506,7 @@ impl LanguageServer for Backend { let ra = self.rust_analyzer.clone(); let mapping = self.mapping.clone(); let documents = self.documents.clone(); + let last_rust_contents = self.last_rust_contents.clone(); let client = self.client.clone(); let previous_diagnostics = self.previous_diagnostics.clone(); let notification_rx = self.notification_rx.clone(); @@ -518,6 +582,10 @@ impl LanguageServer for Backend { .lock() .await .insert(transpiled.rust_path, transpiled.mapping); + last_rust_contents + .lock() + .await + .insert(file.clone(), transpiled.rust_content); } } @@ -552,6 +620,10 @@ impl LanguageServer for Backend { .lock() .await .insert(transpiled.rust_path, transpiled.mapping); + last_rust_contents + .lock() + .await + .insert(syn_path.clone(), transpiled.rust_content); } } } @@ -1076,6 +1148,7 @@ pub async fn start() { mapping: Arc::new(Mutex::new(HashMap::new())), documents: Arc::new(Mutex::new(HashMap::new())), doc_versions: Arc::new(Mutex::new(HashMap::new())), + last_rust_contents: Arc::new(Mutex::new(HashMap::new())), rust_analyzer: Arc::new(Mutex::new( RustAnalyzer::new(tx).expect("Failed to create rust analyzer"), )), From 08e2eff4abf04eaa483da2f4802a3c868a011088 Mon Sep 17 00:00:00 2001 From: Selimaj Dev Date: Mon, 22 Jun 2026 01:28:04 +0200 Subject: [PATCH 11/13] Keyword injection --- crates/analyzer/src/lib.rs | 70 +++++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index 2370c42..f1b71c4 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -1,7 +1,7 @@ pub mod rust_analyzer; pub mod transpiler; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::path::{Component, Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; @@ -19,6 +19,42 @@ use crate::transpiler::transpile_mist; static MARKER_COUNTER: AtomicU64 = AtomicU64::new(0); +const KEYWORDS: [&'static str; 24] = [ + "if", + "else", + "for", + "while", + "match", + "return", + "break", + "continue", + "struct", + "enum", + "class", + "trait", + "impl", + "pub", + "mut", + "let", + "true", + "false", + "dyn", + "loop", + "fn", + "unsafe", + "override", + "module" +]; + +fn keyword_completion_items() -> impl Iterator { + KEYWORDS.into_iter().map(|kw| CompletionItem { + label: kw.to_string(), + kind: Some(CompletionItemKind::KEYWORD), + insert_text: Some(kw.to_string()), + ..Default::default() + }) +} + #[derive(Debug)] struct Backend { client: Client, @@ -793,17 +829,35 @@ impl LanguageServer for Backend { .await { Ok(Some(CompletionResponse::Array(items))) => { - let cleaned: Vec = + let mut cleaned: Vec = items.into_iter().map(clean_completion_item).collect(); + + let existing: HashSet = + cleaned.iter().map(|item| item.label.clone()).collect(); + + cleaned.extend( + keyword_completion_items() + .filter(|item| !existing.contains(&item.label)), + ); + Ok(Some(CompletionResponse::Array(cleaned))) } - Ok(Some(CompletionResponse::List(list))) => { - let cleaned: Vec = + + Ok(Some(CompletionResponse::List(mut list))) => { + let mut cleaned: Vec = list.items.into_iter().map(clean_completion_item).collect(); - Ok(Some(CompletionResponse::List(CompletionList { - is_incomplete: list.is_incomplete, - items: cleaned, - }))) + + let existing: HashSet = + cleaned.iter().map(|item| item.label.clone()).collect(); + + cleaned.extend( + keyword_completion_items() + .filter(|item| !existing.contains(&item.label)), + ); + + list.items = cleaned; + + Ok(Some(CompletionResponse::List(list))) } Ok(None) => Ok(None), Err(e) => { From e58a4a85242051262e079f1dc62eb082519ca198 Mon Sep 17 00:00:00 2001 From: Selimaj Dev Date: Mon, 22 Jun 2026 01:29:11 +0200 Subject: [PATCH 12/13] Added void --- crates/analyzer/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index f1b71c4..c74ad84 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -19,7 +19,7 @@ use crate::transpiler::transpile_mist; static MARKER_COUNTER: AtomicU64 = AtomicU64::new(0); -const KEYWORDS: [&'static str; 24] = [ +const KEYWORDS: [&'static str; 25] = [ "if", "else", "for", @@ -43,7 +43,8 @@ const KEYWORDS: [&'static str; 24] = [ "fn", "unsafe", "override", - "module" + "module", + "void" ]; fn keyword_completion_items() -> impl Iterator { From b257ff5c1a7cb178e6e0b44ba9786b2237a9ce92 Mon Sep 17 00:00:00 2001 From: Selimaj Dev Date: Mon, 22 Jun 2026 01:49:02 +0200 Subject: [PATCH 13/13] Fixed syntax errors --- Cargo.lock | 1 + crates/analyzer/Cargo.toml | 1 + crates/analyzer/src/lib.rs | 135 ++++++++++++++++++++++++++++++++----- 3 files changed, 122 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 57ab2c8..0867479 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -476,6 +476,7 @@ dependencies = [ "dashmap 6.1.0", "mist-codegen", "mist-parser", + "pest", "ropey", "serde", "serde_json", diff --git a/crates/analyzer/Cargo.toml b/crates/analyzer/Cargo.toml index 28072c3..c8445e3 100644 --- a/crates/analyzer/Cargo.toml +++ b/crates/analyzer/Cargo.toml @@ -11,6 +11,7 @@ categories = ["compilers"] [dependencies] mist-parser = { workspace = true } mist-codegen = { workspace = true } +pest = "2.8.6" dashmap = "6.1.0" tokio = { version = "1.52.3", features = ["full", "macros", "rt", "rt-multi-thread"] } diff --git a/crates/analyzer/src/lib.rs b/crates/analyzer/src/lib.rs index c74ad84..45a5dd9 100644 --- a/crates/analyzer/src/lib.rs +++ b/crates/analyzer/src/lib.rs @@ -6,7 +6,9 @@ use std::path::{Component, Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; +use mist_parser::error::ParseError; use mist_parser::rev_mapper::{Mapping, MistMap, RustMap}; +use mist_parser::parse; use ropey::Rope; use serde::Deserialize; use serde_json::Value; @@ -229,6 +231,102 @@ fn rust_uri_to_mist_uri(rust_uri: &Url) -> Option { clean_lsp_url(&mist_path) } +fn byte_offset_to_lsp_pos(source: &str, offset: usize) -> Position { + let mut line = 0u32; + let mut col = 0u32; + for (i, ch) in source.char_indices() { + if i >= offset { + break; + } + if ch == '\n' { + line += 1; + col = 0; + } else { + col += 1; + } + } + Position { + line, + character: col, + } +} + +/// Re-parse the source to extract a proper LSP diagnostic from the error. +/// Returns None when the error is a semantic check failure (can be multiple +/// errors) or when re-parsing unexpectedly succeeds. +fn transpile_error_to_diagnostic(source: &str) -> Option { + match parse(source) { + Err(ParseError::PreAst(pest_err)) => { + let (line, col) = match pest_err.line_col { + pest::error::LineColLocation::Pos((l, c)) => (l, c), + pest::error::LineColLocation::Span((l, c), _) => (l, c), + }; + let message = match &pest_err.variant { + pest::error::ErrorVariant::ParsingError { + positives, + negatives, + } => { + let mut msg = String::new(); + if !positives.is_empty() { + msg.push_str("expected "); + for (i, r) in positives.iter().enumerate() { + if i > 0 { + msg.push_str(" or "); + } + msg.push_str(&format!("{r:?}")); + } + } + if !negatives.is_empty() { + if !msg.is_empty() { + msg.push_str(", "); + } + msg.push_str("unexpected "); + for (i, r) in negatives.iter().enumerate() { + if i > 0 { + msg.push_str(" or "); + } + msg.push_str(&format!("{r:?}")); + } + } + if msg.is_empty() { + msg.push_str("parse error"); + } + msg + } + pest::error::ErrorVariant::CustomError { message } => message.clone(), + }; + Some(Diagnostic { + range: Range { + start: Position { + line: line as u32 - 1, + character: col as u32 - 1, + }, + end: Position { + line: line as u32 - 1, + character: col as u32, + }, + }, + severity: Some(DiagnosticSeverity::ERROR), + source: Some("mist".to_string()), + message, + ..Default::default() + }) + } + Err(ParseError::Ast(ast_err)) => { + let start = byte_offset_to_lsp_pos(source, ast_err.span.start()); + let end = byte_offset_to_lsp_pos(source, ast_err.span.end()); + Some(Diagnostic { + range: Range { start, end }, + severity: Some(DiagnosticSeverity::ERROR), + source: Some("mist".to_string()), + message: ast_err.error_message.clone(), + ..Default::default() + }) + } + Ok(_) => None, + } +} + impl Backend { /// Resolve a Mist cursor position to a Rust position by injecting a unique marker /// into the source at the cursor, transpiling, and finding the marker in the output. @@ -390,22 +488,24 @@ impl Backend { Ok(t) => t, Err(e) => { eprintln!("transpile error for {:?}: {e}", mist_path); - let diag = Diagnostic { - range: Range { - start: Position { - line: 0, - character: 0, + let diag = transpile_error_to_diagnostic(source).unwrap_or_else(|| { + Diagnostic { + range: Range { + start: Position { + line: 0, + character: 0, + }, + end: Position { + line: 0, + character: 1, + }, }, - end: Position { - line: 0, - character: 1, - }, - }, - severity: Some(DiagnosticSeverity::ERROR), - source: Some("mist".to_string()), - message: format!("Transpile error: {e}"), - ..Default::default() - }; + 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; } @@ -436,6 +536,11 @@ impl Backend { .await .insert(mist_path.to_path_buf(), transpiled.rust_content); } + + // Clear any previous diagnostics — transpile succeeded. + if let Some(mist_uri) = clean_lsp_url(mist_path) { + self.publish_diagnostics(mist_uri, vec![]).await; + } } async fn rebuild_module_tree(&self) {