use std::sync::Arc; use tokio::sync::RwLock; use tower_lsp::{Client, LspService, Server}; use tower_lsp::lsp_types as lsp; mod analysis_db; mod rebuild; use analysis_db::SharedDb; struct Backend { db: SharedDb, client: Client, } #[tower_lsp::async_trait] impl tower_lsp::LanguageServer for Backend { async fn initialize( &self, _: tower_lsp::lsp_types::InitializeParams, ) -> tower_lsp::jsonrpc::Result { Ok(tower_lsp::lsp_types::InitializeResult { capabilities: tower_lsp::lsp_types::ServerCapabilities { text_document_sync: Some( tower_lsp::lsp_types::TextDocumentSyncCapability::Kind( tower_lsp::lsp_types::TextDocumentSyncKind::FULL, ), ), // MVP capabilities only (PR-08): definition_provider: Some(tower_lsp::lsp_types::OneOf::Left(true)), document_symbol_provider: Some(tower_lsp::lsp_types::OneOf::Left(true)), // workspace_symbol is not available in tower-lsp 0.20 trait ..Default::default() }, ..Default::default() }) } async fn initialized(&self, _: tower_lsp::lsp_types::InitializedParams) {} async fn shutdown(&self) -> tower_lsp::jsonrpc::Result<()> { Ok(()) } // didOpen: upsert texto, solicita rebuild async fn did_open(&self, params: tower_lsp::lsp_types::DidOpenTextDocumentParams) { let uri = params.text_document.uri.to_string(); let text = params.text_document.text; { let mut guard = self.db.write().await; guard.file_db.upsert(&uri, text); } rebuild::request_rebuild(self.db.clone(), self.client.clone()).await; } // didChange (FULL): receber conteúdo completo e upsert async fn did_change(&self, params: tower_lsp::lsp_types::DidChangeTextDocumentParams) { let uri = params.text_document.uri.to_string(); // Full-sync: esperamos 1 conteúdo completo if let Some(change) = params.content_changes.into_iter().last() { let text = change.text; let mut guard = self.db.write().await; guard.file_db.upsert(&uri, text); } rebuild::request_rebuild(self.db.clone(), self.client.clone()).await; } // didClose: opcionalmente remover do db e limpar diagnostics async fn did_close(&self, params: tower_lsp::lsp_types::DidCloseTextDocumentParams) { let uri = params.text_document.uri; // Estratégia simples: manter FileDB para estabilidade de IDs, mas limpar diagnostics let _ = self .client .publish_diagnostics(uri.clone(), vec![], Some(0)) .await; } async fn goto_definition( &self, params: tower_lsp::lsp_types::GotoDefinitionParams, ) -> tower_lsp::jsonrpc::Result> { let tdp = params.text_document_position_params; let uri = tdp.text_document.uri; let pos = tdp.position; let guard = self.db.read().await; // Map URI to current text and index let Some(fid) = guard.file_db.file_id(uri.as_str()) else { return Ok(None) }; let text = guard.file_db.text(fid).to_string(); let idx = prometeu_analysis::TextIndex::new(&text); let byte = idx.lsp_to_byte(pos.line, pos.character); let ident = ident_at(&text, byte); if let Some(name) = ident { if let Some(snap) = &guard.last_good { let mut hits: Vec = Vec::new(); for s in &snap.symbols_flat { if s.name == name { hits.push(s.location.clone()); } } if !hits.is_empty() { return Ok(Some(lsp::GotoDefinitionResponse::Array(hits))); } } } Ok(None) } // MVP stubs: documentSymbol/workspaceSymbol/definition retornam vazio até PRs seguintes async fn document_symbol( &self, params: tower_lsp::lsp_types::DocumentSymbolParams, ) -> tower_lsp::jsonrpc::Result> { let uri = params.text_document.uri; let guard = self.db.read().await; if let Some(snap) = &guard.last_good { let mut items: Vec = Vec::new(); for s in &snap.symbols_flat { if s.location.uri == uri { items.push(lsp::SymbolInformation { name: s.name.clone(), kind: s.kind, location: s.location.clone(), tags: None, deprecated: None, container_name: None, }); } } return Ok(Some(lsp::DocumentSymbolResponse::Flat(items))); } Ok(Some(lsp::DocumentSymbolResponse::Flat(vec![]))) } // async fn workspace_symbol( // &self, // params: lsp::WorkspaceSymbolParams, // ) -> tower_lsp::jsonrpc::Result>> { // let query = params.query.to_lowercase(); // let guard = self.db.read().await; // if let Some(snap) = &guard.last_good { // let mut out: Vec = Vec::new(); // for s in &snap.symbols_flat { // if s.name.to_lowercase().contains(&query) { // out.push(lsp::SymbolInformation { // name: s.name.clone(), // kind: s.kind, // location: s.location.clone(), // tags: None, // deprecated: None, // container_name: None, // }); // if out.len() >= 50 { break; } // } // } // return Ok(Some(out)); // } // Ok(Some(vec![])) // } } #[tokio::main] async fn main() { let stdin = tokio::io::stdin(); let stdout = tokio::io::stdout(); let db: SharedDb = Arc::new(RwLock::new(analysis_db::AnalysisDb::default())); let (service, socket) = LspService::new(|client| Backend { db: db.clone(), client }); Server::new(stdin, stdout, socket).serve(service).await; } // Simple textual identifier extraction for MVP definition lookup. fn ident_at(text: &str, byte: u32) -> Option { let b = byte as usize; if b > text.len() { return None; } // Expand left and right over identifier characters (ASCII + underscore; acceptable MVP) let bytes = text.as_bytes(); let mut start = b; while start > 0 { let c = bytes[start - 1]; if (c as char).is_ascii_alphanumeric() || c == b'_' { start -= 1; } else { break; } } let mut end = b; while end < bytes.len() { let c = bytes[end]; if (c as char).is_ascii_alphanumeric() || c == b'_' { end += 1; } else { break; } } if start < end { Some(text[start..end].to_string()) } else { None } }