use tokio_util::sync::CancellationToken; use tower_lsp::Client; use crate::analysis_db::{AnalysisSnapshot, SharedDb}; use prometeu_analysis::{TextIndex}; use tower_lsp::lsp_types as lsp; use prometeu_analysis::ids::FileId; use crate::rebuild::compiler_bridge::{ParserFacade, Severity}; #[derive(Clone, Debug)] pub struct LspDiagnostic { pub range: lsp::Range, pub severity: Option, pub code: Option, pub message: String, } #[derive(Clone, Debug)] pub struct FlatSymbol { pub name: String, pub kind: lsp::SymbolKind, pub location: lsp::Location, } /// Requests a project rebuild (coarse). Cancels the previous rebuild if in progress. pub async fn request_rebuild(db: SharedDb, client: Client) { // 1) short lock: cancel previous token and install a new one let new_token = CancellationToken::new(); { let mut guard = db.write().await; if let Some(prev) = guard.active_rebuild.take() { prev.cancel(); } guard.active_rebuild = Some(new_token.clone()); } // 2) spawn task: run analysis outside the lock tokio::spawn(async move { // Safe point: check before starting if new_token.is_cancelled() { return; } // Clone snapshot of files (URIs and texts) under a short read lock let (files, revision) = { let guard = db.read().await; let mut v = Vec::new(); for fid in guard.file_ids() { let uri = guard.file_db.uri(fid).to_string(); let text = guard.file_db.text(fid).to_string(); v.push((fid, uri, text)); } (v, guard.revision) }; // Prepare accumulators let mut diagnostics_by_uri: std::collections::HashMap> = std::collections::HashMap::new(); let mut symbols_flat: Vec = Vec::new(); // For each file: run a minimal frontend to collect diagnostics and top-level symbols for (fid, uri, text) in files.into_iter() { if new_token.is_cancelled() { return; } let text_index = TextIndex::new(&text); // Parser + basic pipeline let mut interner = prometeu_analysis::NameInterner::new(); let mut parser = ParserFacade::new(&text, fid, &mut interner); match parser.parse_and_collect() { Ok(parsed) => { // Diagnostics (from parse/collect are already inside parsed.diags) let mut file_diags = Vec::new(); for d in parsed.diagnostics { let range = span_to_range(fid, &text_index, d.span.start, d.span.end); file_diags.push(LspDiagnostic { range, severity: Some(match d.severity { Severity::Error => lsp::DiagnosticSeverity::ERROR, Severity::Warning => lsp::DiagnosticSeverity::WARNING }), code: Some(lsp::NumberOrString::String(d.code)), message: d.message, }); } diagnostics_by_uri.insert(uri.clone(), file_diags); // Symbols: flatten only top-level decls with their decl_span for sym in parsed.symbols { let lsp_loc = lsp::Location { uri: uri.parse().unwrap_or_else(|_| lsp::Url::parse("untitled:").unwrap()), range: span_to_range(fid, &text_index, sym.decl_span.start, sym.decl_span.end), }; let kind = match sym.kind { prometeu_compiler::analysis::symbols::SymbolKind::Function => lsp::SymbolKind::FUNCTION, prometeu_compiler::analysis::symbols::SymbolKind::Service => lsp::SymbolKind::INTERFACE, prometeu_compiler::analysis::symbols::SymbolKind::Struct => lsp::SymbolKind::STRUCT, prometeu_compiler::analysis::symbols::SymbolKind::Contract => lsp::SymbolKind::CLASS, prometeu_compiler::analysis::symbols::SymbolKind::ErrorType => lsp::SymbolKind::ENUM, _ => lsp::SymbolKind::VARIABLE, }; symbols_flat.push(FlatSymbol { name: sym.name, kind, location: lsp_loc }); } } Err(diags) => { // Parser returned errors only; publish them let mut file_diags = Vec::new(); for d in diags { let range = span_to_range(fid, &text_index, d.span.start, d.span.end); file_diags.push(LspDiagnostic { range, severity: Some(match d.severity { Severity::Error => lsp::DiagnosticSeverity::ERROR, Severity::Warning => lsp::DiagnosticSeverity::WARNING }), code: Some(lsp::NumberOrString::String(d.code)), message: d.message, }); } diagnostics_by_uri.insert(uri.clone(), file_diags); } } } if new_token.is_cancelled() { return; } // 3) short lock: swap state + revision++ if not cancelled; then publish diagnostics let snapshot = AnalysisSnapshot { diagnostics_by_uri: diagnostics_by_uri.clone(), symbols_flat }; { let mut guard = db.write().await; if new_token.is_cancelled() { return; } // if no new changes since we started, accept this snapshot guard.last_good = Some(snapshot); guard.revision = revision.saturating_add(1); } // Publish diagnostics per file for (uri, diags) in diagnostics_by_uri.into_iter() { let lsp_diags: Vec = diags.into_iter().map(|d| lsp::Diagnostic { range: d.range, severity: d.severity, code: d.code, message: d.message, ..Default::default() }).collect(); let _ = client.publish_diagnostics(uri.parse().unwrap_or_else(|_| lsp::Url::parse("untitled:").unwrap()), lsp_diags, None).await; } }); } fn span_to_range(file: FileId, idx: &TextIndex, start: u32, end: u32) -> lsp::Range { // Ignore `file` here since idx is built from that file's text let (s_line, s_col) = idx.byte_to_lsp(start); let (e_line, e_col) = idx.byte_to_lsp(end); lsp::Range { start: lsp::Position { line: s_line, character: s_col }, end: lsp::Position { line: e_line, character: e_col }, } } /// Minimal integration with the compiler frontend for the MVP rebuild loop. mod compiler_bridge { use super::*; use prometeu_compiler::frontends::pbs as p; use prometeu_compiler::common::spans as cspans; use prometeu_compiler::common::diagnostics as cdiag; #[derive(Clone, Debug)] pub enum Severity { Error, Warning } #[derive(Clone, Debug)] pub struct Diag { pub severity: Severity, pub code: String, pub message: String, pub span: cspans::Span } #[derive(Clone, Debug)] pub struct SymbolItem { pub name: String, pub kind: prometeu_compiler::analysis::symbols::SymbolKind, pub decl_span: cspans::Span } #[derive(Clone, Debug)] pub struct ParsedResult { pub diagnostics: Vec, pub symbols: Vec } pub struct ParserFacade<'a> { text: &'a str, file_id: FileId, interner: &'a mut prometeu_analysis::NameInterner, } impl<'a> ParserFacade<'a> { pub fn new(text: &'a str, file_id: FileId, interner: &'a mut prometeu_analysis::NameInterner) -> Self { Self { text, file_id, interner } } pub fn parse_and_collect(&mut self) -> Result> { let mut parser = p::parser::Parser::new(self.text, cspans::FileId(self.file_id.0), self.interner); let parsed = match parser.parse_file() { Ok(p) => p, Err(bundle) => { let diags = bundle.diagnostics.into_iter().map(|d| Diag { severity: match d.severity { cdiag::Severity::Error => Severity::Error, cdiag::Severity::Warning => Severity::Warning }, code: d.code, message: d.message, span: d.span }).collect(); return Err(diags); } }; let mut collector = p::collector::SymbolCollector::new(self.interner); let (type_symbols, value_symbols) = match collector.collect(&parsed.arena, parsed.root) { Ok(v) => v, Err(bundle) => { let diags = bundle.diagnostics.into_iter().map(|d| Diag { severity: match d.severity { cdiag::Severity::Error => Severity::Error, cdiag::Severity::Warning => Severity::Warning }, code: d.code, message: d.message, span: d.span }).collect(); return Err(diags); } }; let module_symbols = p::symbols::ModuleSymbols { type_symbols, value_symbols }; struct EmptyProvider; impl p::resolver::ModuleProvider for EmptyProvider { fn get_module_symbols(&self, _from_path: &str) -> Option<&p::symbols::ModuleSymbols> { None } } let mut resolver = p::resolver::Resolver::new(&module_symbols, &EmptyProvider, self.interner); // bootstrap primitives using a throwaway interner behavior resolver.bootstrap_types(self.interner); if let Err(bundle) = resolver.resolve(&parsed.arena, parsed.root) { let diags = bundle.diagnostics.into_iter().map(|d| Diag { severity: match d.severity { cdiag::Severity::Error => Severity::Error, cdiag::Severity::Warning => Severity::Warning }, code: d.code, message: d.message, span: d.span }).collect(); return Err(diags); } // Collect top-level symbols only for MVP let mut symbols = Vec::new(); for s in &resolver.symbol_arena.symbols { // Keep only decls in this file if s.decl_span.file.0 == self.file_id.0 { let name = self.interner.resolve(s.name).to_string(); let kind = s.kind; symbols.push(SymbolItem { name, kind, decl_span: s.decl_span.clone() }); } } Ok(ParsedResult { diagnostics: vec![], symbols }) } } }