//! LSP API endpoints — completions, hover, errors, activate-preview. use axum::{ extract::{Query, State}, http::StatusCode, Json, }; use serde::{Deserialize, Serialize}; use el_lsp::{Completion, Diagnostic, HoverInfo, LanguageServer}; use crate::AppState; type ApiResult = Result, (StatusCode, Json)>; // ── Query types ─────────────────────────────────────────────────────────────── #[derive(Debug, Deserialize)] pub struct SourceQuery { pub source: String, pub pos: Option, } #[derive(Debug, Deserialize)] pub struct ActivatePreviewQuery { pub type_name: String, pub query: String, pub source: Option, } #[derive(Debug, Serialize)] pub struct ActivatePreviewResponse { pub count: usize, pub nodes: Vec, pub connected: bool, } #[derive(Debug, Serialize)] pub struct ActivateNode { pub id: String, pub label: String, pub score: f64, } // ── Handlers ────────────────────────────────────────────────────────────────── /// GET /api/lsp/complete?source=...&pos=... pub async fn complete( Query(q): Query, ) -> ApiResult> { let lsp = LanguageServer::new(); let pos = q.pos.unwrap_or(0); let completions = lsp.complete(&q.source, pos); Ok(Json(completions)) } /// GET /api/lsp/hover?source=...&pos=... pub async fn hover( Query(q): Query, ) -> ApiResult> { let lsp = LanguageServer::new(); let pos = q.pos.unwrap_or(0); let info = lsp.hover(&q.source, pos); Ok(Json(info)) } /// GET /api/lsp/errors?source=... pub async fn errors( Query(q): Query, ) -> ApiResult> { let lsp = LanguageServer::new(); let diags = lsp.diagnostics(&q.source); Ok(Json(diags)) } /// GET /api/lsp/activate-preview?type_name=...&query=...&source=... /// /// Returns a live preview of what nodes would activate for the given /// `activate TypeName where "query"` expression. pub async fn activate_preview( State(state): State, Query(q): Query, ) -> ApiResult { let engram_url = &state.config.engram_url; // Try to query the Engram DB for matching nodes let client = reqwest::Client::new(); let url = format!("{engram_url}/api/activate-preview"); let body = serde_json::json!({ "type_name": q.type_name, "query": q.query, "limit": 5, }); match client .post(&url) .json(&body) .timeout(std::time::Duration::from_secs(5)) .send() .await { Ok(resp) if resp.status().is_success() => { let json: serde_json::Value = resp.json().await.unwrap_or_default(); let nodes: Vec = json["nodes"] .as_array() .map(|arr| { arr.iter() .map(|n| ActivateNode { id: n["id"].as_str().unwrap_or("").to_string(), label: n["label"].as_str().unwrap_or("").to_string(), score: n["score"].as_f64().unwrap_or(0.0), }) .collect() }) .unwrap_or_default(); let count = json["count"].as_u64().unwrap_or(nodes.len() as u64) as usize; Ok(Json(ActivatePreviewResponse { count, nodes, connected: true, })) } _ => { // Engram not connected — return stub indicating disconnected state Ok(Json(ActivatePreviewResponse { count: 0, nodes: vec![], connected: false, })) } } }