2026-03-24 13:40:29 +00:00

193 lines
7.1 KiB
Rust

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<tower_lsp::lsp_types::InitializeResult> {
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<Option<tower_lsp::lsp_types::GotoDefinitionResponse>> {
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<lsp::Location> = 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<Option<tower_lsp::lsp_types::DocumentSymbolResponse>> {
let uri = params.text_document.uri;
let guard = self.db.read().await;
if let Some(snap) = &guard.last_good {
let mut items: Vec<lsp::SymbolInformation> = 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<Option<Vec<lsp::SymbolInformation>>> {
// let query = params.query.to_lowercase();
// let guard = self.db.read().await;
// if let Some(snap) = &guard.last_good {
// let mut out: Vec<lsp::SymbolInformation> = 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<String> {
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 }
}