Working LSP error checking

This commit is contained in:
2026-05-22 15:53:25 +02:00
parent 52efdda744
commit 9f6ec4a20d
6 changed files with 353 additions and 102 deletions
Generated
+1
View File
@@ -444,6 +444,7 @@ dependencies = [
name = "mist-analyzer" name = "mist-analyzer"
version = "0.0.2" version = "0.0.2"
dependencies = [ dependencies = [
"cargo_metadata",
"dashmap 6.2.1", "dashmap 6.2.1",
"mist-codegen", "mist-codegen",
"mist-parser", "mist-parser",
+3 -1
View File
@@ -4,4 +4,6 @@ cd D:\projects\mist\mist
mist build >nul 2>&1 mist build >nul 2>&1
target\debug\mist-analyzer.exe cd D:\projects\mist\mist-lsp-test
D:\projects\mist\mist\target\debug\mist-analyzer.exe
+1
View File
@@ -15,3 +15,4 @@ mist-codegen = { workspace = true }
dashmap = "6.2.1" dashmap = "6.2.1"
tokio = { version = "1.52.3", features = ["full", "macros", "rt", "rt-multi-thread"] } tokio = { version = "1.52.3", features = ["full", "macros", "rt", "rt-multi-thread"] }
tower-lsp = "0.20.0" tower-lsp = "0.20.0"
cargo_metadata = "0.23.1"
+171
View File
@@ -0,0 +1,171 @@
use std::{
collections::HashMap,
env, fs,
path::{MAIN_SEPARATOR, PathBuf},
process::{Command, Stdio},
};
use cargo_metadata::{CompilerMessage, Message};
use mist_parser::rev_mapper::{RustMap, find_mapping, get_mapping};
#[derive(Debug, Clone)]
pub struct MistDiagnosticMessage {
pub message: String,
pub file_path: PathBuf,
pub file_name: String,
pub line: usize,
pub column: usize,
}
#[derive(Debug, Clone)]
pub enum MistDiagnostic {
Error(MistDiagnosticMessage),
Warning(MistDiagnosticMessage),
Rust(CompilerMessage),
}
pub fn build(mut args: Vec<String>, root: PathBuf) -> Vec<MistDiagnostic> {
args.insert(1, "--message-format=json".to_string());
let is_root = root == env::current_dir().expect("Failed getting env");
let mut command = Command::new("cargo")
.args(args)
.stdout(Stdio::piped())
.spawn()
.expect("Failed to run cargo");
let mut reader = std::io::BufReader::new(command.stdout.take().expect("Failed to get reader"));
let mut diagnostics = Vec::new();
let mut mapping = HashMap::new();
let mist_src = format!(".mist{MAIN_SEPARATOR}lsp");
for message in cargo_metadata::Message::parse_stream(&mut reader) {
match message {
Ok(Message::CompilerMessage(msg)) => {
for span in &msg.message.spans {
if span.is_primary {
let rust_path = root.join(&span.file_name);
let mist_file = span
.file_name
.replacen(&mist_src, "src", 1)
.trim_end_matches(".rs")
.to_string()
+ ".mist";
let mist_path = root.join(&mist_file);
if !fs::exists(&mist_path).expect("Unable to check if mist file exists") {
diagnostics.push(MistDiagnostic::Rust(msg));
break;
}
let map = mapping.entry(rust_path.clone()).or_insert_with(|| {
get_mapping(
&fs::read_to_string(rust_path)
.expect("Failed to read file for mapping"),
)
});
let mist_span =
find_mapping(&map, &RustMap(span.line_end, span.column_start))
.expect("Unable to find mapping");
let mist_msg = MistDiagnosticMessage {
message: span.label.clone().unwrap_or(msg.message.message.clone()),
file_name: if is_root {
mist_file
} else {
mist_path.to_string_lossy().to_string()
},
file_path: mist_path,
line: mist_span.1.0,
column: mist_span.1.1,
};
match msg.message.level {
cargo_metadata::diagnostic::DiagnosticLevel::Error => {
diagnostics.push(MistDiagnostic::Error(mist_msg))
}
cargo_metadata::diagnostic::DiagnosticLevel::Warning => {
diagnostics.push(MistDiagnostic::Warning(mist_msg))
}
_ => {}
}
}
}
}
Ok(Message::BuildFinished(_)) => {
command.wait().unwrap();
return diagnostics;
}
Ok(Message::TextLine(text)) => println!("{text}"),
_ => {}
}
}
diagnostics
}
// pub fn print_diagnostics(diagnostics: &Vec<MistDiagnostic>) {
// let mut files = HashMap::new();
// for diag in diagnostics {
// match diag {
// MistDiagnostic::Error(msg) => {
// let line = get_line(&mut files, &msg);
// println!(
// "\n{}:{}:{}\n \x1b[31mError\x1b[0m: {}\n\t{}",
// msg.file_name,
// msg.line,
// msg.column,
// msg.message,
// line.unwrap_or_default(),
// )
// }
// MistDiagnostic::Warning(msg) => {
// let line = get_line(&mut files, &msg);
// println!(
// "\n{}:{}:{}\n \x1b[33mWarning\x1b[0m: {}\n\t{}",
// msg.file_name,
// msg.line,
// msg.column,
// msg.message,
// line.unwrap_or_default(),
// )
// }
// MistDiagnostic::Rust(rs) => println!("{rs}"),
// }
// }
// }
pub fn get_line(
files: &mut HashMap<PathBuf, Vec<String>>,
msg: &MistDiagnosticMessage,
) -> Option<String> {
let src_path = msg.file_path.clone();
let lines = files.entry(src_path.clone()).or_insert_with(|| {
fs::read_to_string(src_path)
.expect("Unable to read mist file")
.lines()
.into_iter()
.map(String::from)
.collect()
});
lines.get(msg.line - 1).map(|v| v.trim().to_string())
}
+113 -39
View File
@@ -1,3 +1,4 @@
pub mod builder;
pub mod transpiler; pub mod transpiler;
use std::path::{Component, PathBuf}; use std::path::{Component, PathBuf};
@@ -8,6 +9,7 @@ use tower_lsp::jsonrpc::Result;
use tower_lsp::lsp_types::*; use tower_lsp::lsp_types::*;
use tower_lsp::{Client, LanguageServer, LspService, Server}; use tower_lsp::{Client, LanguageServer, LspService, Server};
use crate::builder::MistDiagnostic;
use crate::transpiler::transpile_file; use crate::transpiler::transpile_file;
#[derive(Debug)] #[derive(Debug)]
@@ -21,16 +23,6 @@ impl LanguageServer for Backend {
async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> { async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
let mut res = InitializeResult::default(); let mut res = InitializeResult::default();
// res.capabilities.completion_provider = Some(CompletionOptions {
// resolve_provider: None,
// trigger_characters: None,
// all_commit_characters: None,
// work_done_progress_options: WorkDoneProgressOptions {
// work_done_progress: None,
// },
// completion_item: None,
// });
res.capabilities.text_document_sync = Some(TextDocumentSyncCapability::Options( res.capabilities.text_document_sync = Some(TextDocumentSyncCapability::Options(
TextDocumentSyncOptions { TextDocumentSyncOptions {
open_close: Some(true), open_close: Some(true),
@@ -41,23 +33,21 @@ impl LanguageServer for Backend {
}, },
)); ));
*self.workspace_folder.lock().await = params // Safely extract workspace root without deep nested matching panics
let folder_path = params
.workspace_folders .workspace_folders
.iter() .as_ref()
.next() .and_then(|folders| folders.first())
.map(|folders| { .and_then(|folder| folder.uri.to_file_path().ok());
folders
.iter() if let Some(ref path) = folder_path {
.next() *self.workspace_folder.lock().await = Some(path.clone());
.map(|v| v.uri.to_file_path().ok()) }
.flatten()
})
.flatten();
let workspace_folder = self.workspace_folder.clone(); let workspace_folder = self.workspace_folder.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Some(root) = &*workspace_folder.lock().await { if let Some(root) = &*workspace_folder.lock().await {
// Consideration: Ensure transpiler::build is panic-safe internally
transpiler::build(root); transpiler::build(root);
} }
}); });
@@ -73,22 +63,115 @@ impl LanguageServer for Backend {
async fn did_save(&self, params: DidSaveTextDocumentParams) { async fn did_save(&self, params: DidSaveTextDocumentParams) {
self.client self.client
.log_message(MessageType::INFO, "getting completion") .log_message(MessageType::INFO, "Processing did_save event")
.await; .await;
let input_path = params // 1. Safe URI parsing fallback
.text_document let input_path = match params.text_document.uri.to_file_path() {
.uri Ok(path) => path,
.to_file_path() Err(_) => {
.expect("Invalid document path"); self.client
.log_message(
MessageType::WARNING,
"Skipping: Document URI is not a valid local file path",
)
.await;
return;
}
};
let output_path = from_mist_to_rust(input_path.clone()); let output_path = from_mist_to_rust(input_path.clone());
self.client self.client
.log_message(MessageType::INFO, output_path.display()) .log_message(
MessageType::INFO,
format!("Transpiling: {}", input_path.display()),
)
.await; .await;
transpile_file(&input_path, &output_path); // 2. Safe call to the updated transpile_file function
if let Err(err_msg) = transpile_file(&input_path, &output_path) {
self.client
.log_message(
MessageType::ERROR,
format!("Transpilation error: {err_msg}"),
)
.await;
// Optimization choice: You can return early here, or keep moving forward
// to collect compiler diagnostics anyway.
}
// 3. Safe workspace root retrieval fallback
let workspace_root = match self.workspace_folder.lock().await.clone() {
Some(root) => root,
None => {
self.client
.log_message(
MessageType::WARNING,
"Skipping diagnostics: No active workspace folder found",
)
.await;
return;
}
};
// 4. Run the project builder stage
let diagnostics_raw = builder::build(
vec![
"check".to_string(),
"--bin".to_string(),
"mist-lsp".to_string(),
],
workspace_root,
);
let mut diagnostics = Vec::new();
for diag in diagnostics_raw.iter() {
let (msg, severity) = match diag {
MistDiagnostic::Error(msg) => (msg, DiagnosticSeverity::ERROR),
MistDiagnostic::Warning(msg) => (msg, DiagnosticSeverity::WARNING),
MistDiagnostic::Rust(d) => {
self.client
.log_message(
MessageType::LOG,
format!("Unhandled underlying Rust diagnostic: {d:?}"),
)
.await;
continue;
}
};
// 5. Safe indexing checks via saturating_sub
let line = (msg.line as u32).saturating_sub(1);
let column = (msg.column as u32).saturating_sub(1);
diagnostics.push(Diagnostic {
range: Range {
start: Position {
line,
character: column,
},
end: Position {
line,
character: u32::MAX, // Highlights to the end of the line safely
},
},
severity: Some(severity),
code: None,
source: Some("mist-analyzer".to_string()),
message: msg.message.clone(),
related_information: None,
tags: None,
data: None,
code_description: None,
});
}
// 6. Send the generated diagnostics back to the IDE client interface
self.client
.publish_diagnostics(params.text_document.uri, diagnostics, None)
.await;
} }
async fn shutdown(&self) -> Result<()> { async fn shutdown(&self) -> Result<()> {
@@ -111,22 +194,13 @@ pub async fn start() {
fn from_mist_to_rust(mut path: PathBuf) -> PathBuf { fn from_mist_to_rust(mut path: PathBuf) -> PathBuf {
path.set_extension("rs"); path.set_extension("rs");
// 1. Convert path to a vector of components
let mut comps: Vec<Component> = path.components().collect(); let mut comps: Vec<Component> = path.components().collect();
// 2. Find the index of the *last* component matching "src"
if let Some(pos) = comps.iter().rposition(|c| c.as_os_str() == "src") { if let Some(pos) = comps.iter().rposition(|c| c.as_os_str() == "src") {
// Define your replacement path
let replacement = std::path::Path::new(".mist/lsp"); let replacement = std::path::Path::new(".mist/lsp");
// 3. Splice the replacement components into the original vector,
// replacing the single "src" component at `pos`
comps.splice(pos..=pos, replacement.components()); comps.splice(pos..=pos, replacement.components());
// 4. Rebuild the PathBuf from the modified components
comps.iter().collect() comps.iter().collect()
} else { } else {
// Return path with just the extension changed if "src" wasn't found
path path
} }
} }
+64 -62
View File
@@ -1,7 +1,6 @@
use std::{ use std::{
fs, fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
process,
}; };
use mist_parser::error::ParseError; use mist_parser::error::ParseError;
@@ -10,42 +9,50 @@ pub fn build(root: &PathBuf) {
let src_dir = root.join("src"); let src_dir = root.join("src");
let out_dir = root.join(".mist/lsp"); let out_dir = root.join(".mist/lsp");
build_dir(&root, &src_dir, &src_dir, &out_dir); if let Err(e) = build_dir(root, &src_dir, &src_dir, &out_dir) {
eprintln!("Warning: Build directory run aborted safely: {e}");
}
} }
fn build_dir(root: &Path, base_src: &Path, current_dir: &Path, out_dir: &Path) { fn build_dir(
let entries = match fs::read_dir(current_dir) { root: &Path,
Ok(entries) => entries, base_src: &Path,
Err(e) => { current_dir: &Path,
eprintln!( out_dir: &Path,
"error: failed to read directory {}\n {}", ) -> Result<(), String> {
current_dir.display(), let entries = fs::read_dir(current_dir)
e .map_err(|e| format!("failed to read directory {}: {}", current_dir.display(), e))?;
);
process::exit(1);
}
};
for entry in entries { for entry in entries {
let entry = match entry { let entry = match entry {
Ok(entry) => entry, Ok(entry) => entry,
Err(e) => { Err(e) => {
eprintln!("error: failed to read directory entry\n {}", e); eprintln!("Warning: Skipping invalid directory entry: {e}");
continue; // Skip corrupted entry instead of crashing
process::exit(1);
} }
}; };
let path = entry.path(); let path = entry.path();
// recurse into nested directories // Recurse into nested directories
if path.is_dir() { if path.is_dir() {
build_dir(root, base_src, &path, out_dir); if let Err(e) = build_dir(root, base_src, &path, out_dir) {
eprintln!("Warning: Nested build directory failed: {e}");
}
continue; continue;
} }
let relative = path.strip_prefix(base_src).unwrap(); // Safe prefix stripping fallback
let relative = match path.strip_prefix(base_src) {
Ok(p) => p,
Err(_) => {
eprintln!(
"Warning: Path {} is outside base source directory",
path.display()
);
continue;
}
};
// Handle non-mist files with a cache check // Handle non-mist files with a cache check
if path.extension().and_then(|e| e.to_str()) != Some("mist") { if path.extension().and_then(|e| e.to_str()) != Some("mist") {
@@ -60,7 +67,13 @@ fn build_dir(root: &Path, base_src: &Path, current_dir: &Path, out_dir: &Path) {
continue; continue;
} }
fs::copy(&path, dest_path).expect("Failed to copy non-mist file"); if let Err(e) = fs::copy(&path, &dest_path) {
eprintln!(
"Warning: Failed to copy non-mist file {}: {}",
path.display(),
e
);
}
continue; continue;
} }
@@ -71,75 +84,64 @@ fn build_dir(root: &Path, base_src: &Path, current_dir: &Path, out_dir: &Path) {
continue; continue;
} }
transpile_file(&path, &output_path); if let Err(e) = transpile_file(&path, &output_path) {
}
}
pub fn transpile_file(path: &Path, output_path: &Path) {
// create parent directories
if let Some(parent) = output_path.parent() {
if let Err(e) = fs::create_dir_all(parent) {
eprintln!( eprintln!(
"error: failed to create output directory {}\n {}", "Warning: Transpilation failed for {}: {}",
parent.display(), path.display(),
e e
); );
process::exit(1);
} }
} }
// read source Ok(())
let source = match fs::read_to_string(&path) { }
Ok(s) => s,
Err(e) => {
eprintln!("error: failed to read file {}\n {}", path.display(), e);
process::exit(1); pub fn transpile_file(path: &Path, output_path: &Path) -> Result<(), String> {
} // Create parent directories
}; if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent).map_err(|e| {
format!(
"failed to create output directory {}: {}",
parent.display(),
e
)
})?;
}
// Read source
let source = fs::read_to_string(path)
.map_err(|e| format!("failed to read file {}: {}", path.display(), e))?;
let parser_result = mist_parser::parse(&source).map_err(|e| match e { let parser_result = mist_parser::parse(&source).map_err(|e| match e {
ParseError::Ast(e) => { ParseError::Ast(e) => {
// Using components carefully to prevent out-of-bounds or zero layout crashes
let start_pos = e.span.start_pos().line_col(); let start_pos = e.span.start_pos().line_col();
let span = e.span.as_str(); let span = e.span.as_str();
format!( format!(
"\n{}:{}:{}\n \x1b[31mError\x1b[0m: {}\n\t{}{}\t{}", "\n{}:{}:{}\n Error: {}\n\t{}{}\t{}",
path.as_os_str().display(), path.display(),
start_pos.0, start_pos.0,
start_pos.1, start_pos.1,
e.error_message, e.error_message,
span, span,
if span.ends_with("\n") { "" } else { "\n" }, if span.ends_with('\n') { "" } else { "\n" },
"^".repeat(span.trim().len()), "^".repeat(span.trim().len()),
) )
} }
ParseError::PreAst(e) => format!("{e}"), ParseError::PreAst(e) => format!("{e}"),
}); });
let ast = match parser_result { // If parsing fails, return the error string back gracefully so the LSP can show it
Ok(ast) => ast, let ast = parser_result.map_err(|e| format!("parse failed in {}:\n{}", path.display(), e))?;
Err(e) => {
eprintln!("error: parse failed in {}\n{}", path.display(), e);
process::exit(1);
}
};
let mut gc = mist_codegen::RustCodegen::new(); let mut gc = mist_codegen::RustCodegen::new();
let output = gc.generate(ast); let output = gc.generate(ast);
if let Err(e) = fs::write(&output_path, output) { fs::write(output_path, output)
eprintln!( .map_err(|e| format!("failed to write output {}: {}", output_path.display(), e))?;
"error: failed to write output {}\n {}",
output_path.display(),
e
);
process::exit(1); Ok(())
}
} }
fn should_skip(source: &Path, output: &Path) -> bool { fn should_skip(source: &Path, output: &Path) -> bool {