use std::path::Path;
use serde_json::{Value, json};
use crate::lsp::manager::LspManager;
use crate::lsp::query::{self, Operation};
#[derive(serde::Deserialize)]
struct Request {
op: String,
file: String,
#[serde(default)]
line: u32,
#[serde(default)]
char: u32,
#[serde(default)]
query: String,
}
pub async fn run_query(manager: &LspManager, request_json: &str) -> String {
let req: Request = match serde_json::from_str(request_json) {
Ok(r) => r,
Err(e) => return json!({ "error": format!("invalid lsp request: {e}") }).to_string(),
};
let path = Path::new(&req.file);
if req.op == "diagnostics" {
let diags = manager
.diagnostics_for(path)
.or_else(|| {
path.canonicalize()
.ok()
.and_then(|c| manager.diagnostics_for(&c))
})
.unwrap_or_default();
return json!(diags).to_string();
}
match Operation::parse(&req.op) {
Some(op) => {
let result: Value = query::run(manager, op, path, req.line, req.char, &req.query).await;
result.to_string()
}
None => json!({ "error": format!("unknown lsp op: {}", req.op) }).to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lsp::manager::LspManager;
use crate::lsp::spawn::{Spawned, Spawner};
use futures::future::BoxFuture;
use std::sync::Arc;
struct NoServerSpawner;
impl Spawner for NoServerSpawner {
fn spawn<'a>(
&'a self,
_server_id: &'a str,
_root: &'a Path,
) -> BoxFuture<'a, std::io::Result<Spawned>> {
Box::pin(async { Err(std::io::Error::other("no server in test")) })
}
}
fn test_manager() -> LspManager {
LspManager::new(Arc::new(NoServerSpawner), std::env::temp_dir())
}
#[tokio::test]
async fn invalid_json_returns_error_object() {
let out = run_query(&test_manager(), "not json at all").await;
let v: Value = serde_json::from_str(&out).unwrap();
assert!(
v.get("error")
.and_then(|e| e.as_str())
.is_some_and(|s| s.contains("invalid lsp request")),
"got {out}"
);
}
#[tokio::test]
async fn unknown_op_returns_error_object() {
let req = json!({ "op": "frobnicate", "file": "/tmp/none.xyz" }).to_string();
let out = run_query(&test_manager(), &req).await;
let v: Value = serde_json::from_str(&out).unwrap();
assert_eq!(
v.get("error").and_then(|e| e.as_str()),
Some("unknown lsp op: frobnicate"),
"got {out}"
);
}
#[tokio::test]
async fn accepts_operation_aliases_shared_with_the_lsp_tool() {
let req = json!({ "op": "goToDefinition", "file": "/tmp/none.xyz", "line": 1, "char": 1 })
.to_string();
let out = run_query(&test_manager(), &req).await;
let v: Value = serde_json::from_str(&out).unwrap();
let err = v.get("error").and_then(|e| e.as_str()).unwrap_or("");
assert!(!err.contains("unknown"), "alias should resolve, got {out}");
}
#[tokio::test]
async fn diagnostics_for_untracked_file_is_empty_array() {
let req = json!({ "op": "diagnostics", "file": "/tmp/never-opened.rs" }).to_string();
let out = run_query(&test_manager(), &req).await;
assert_eq!(out, "[]", "got {out}");
}
}