agentmux_srv/server/
lsp_handlers.rs1use std::sync::Arc;
5
6use crate::backend::rpc::engine::WshRpcEngine;
7use crate::backend::base::expand_home_dir_safe;
8use crate::backend::rpc_types::{LspSendReq, LspStartReq, LspStartResult, LspStopReq};
9
10use super::AppState;
11
12pub fn register_lsp_handlers(engine: &Arc<WshRpcEngine>, state: &AppState) {
13 let lsp_supervisor_start = state.lsp_supervisor.clone();
23 engine.register_typed(
24 "lspstart",
25 move |cmd: LspStartReq, _ctx| {
26 let supervisor = lsp_supervisor_start.clone();
27 async move {
28 let expanded = expand_home_dir_safe(&cmd.file_path);
29 let workspace_root =
30 crate::backend::lsp::workspace::detect_workspace_root(expanded.as_path());
31 let result = supervisor
32 .start(crate::backend::lsp::StartArgs {
33 language: cmd.language,
34 workspace_root,
35 })
36 .await
37 .map_err(|e| e.to_wire_string())?;
38 Ok(LspStartResult {
39 server_id: result.server_id,
40 workspace_root: result.workspace_root,
41 })
42 }
43 },
44 );
45
46 let lsp_supervisor_send = state.lsp_supervisor.clone();
47 engine.register_typed(
48 "lspsend",
49 move |cmd: LspSendReq, _ctx| {
50 let supervisor = lsp_supervisor_send.clone();
51 async move {
52 let message_json = serde_json::to_string(&cmd.message)
53 .map_err(|e| format!("lspsend serialize: {e}"))?;
54 supervisor
55 .send(&cmd.server_id, &message_json)
56 .await
57 .map_err(|e| e.to_wire_string())?;
58 Ok(())
59 }
60 },
61 );
62
63 let lsp_supervisor_stop = state.lsp_supervisor.clone();
64 engine.register_typed(
65 "lspstop",
66 move |cmd: LspStopReq, _ctx| {
67 let supervisor = lsp_supervisor_stop.clone();
68 async move {
69 supervisor
70 .stop(&cmd.server_id)
71 .await
72 .map_err(|e| e.to_wire_string())?;
73 Ok(())
74 }
75 },
76 );
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::backend::rpc_types::RpcMessage;
83
84 #[tokio::test]
91 async fn register_typed_records_the_three_lsp_commands() {
92 let state = crate::server::tests::test_state();
93 let (engine, _rx) = WshRpcEngine::new();
94 register_lsp_handlers(&engine, &state);
95 let schema = engine.schema_json();
96 let rows = schema.as_array().unwrap();
97 let find = |cmd: &str| {
98 rows.iter()
99 .find(|r| r["command"] == cmd)
100 .unwrap_or_else(|| panic!("{cmd} missing from the schema"))
101 };
102
103 let start = find("lspstart");
104 assert_eq!(start["requestName"], "LspStartReq");
105 assert_eq!(start["responseName"], "LspStartResult");
106
107 for cmd in ["lspsend", "lspstop"] {
111 let row = find(cmd);
112 assert_eq!(row["responseName"], "()", "{cmd} should answer nothing");
113 }
114 assert_eq!(find("lspsend")["requestName"], "LspSendReq");
115 assert_eq!(find("lspstop")["requestName"], "LspStopReq");
116 }
117
118 #[test]
124 fn lspsend_accepts_any_json_message() {
125 for message in [
126 serde_json::json!({ "jsonrpc": "2.0", "id": 1, "method": "initialize" }),
127 serde_json::json!([1, 2, 3]),
128 serde_json::json!("a bare string"),
129 serde_json::Value::Null,
130 ] {
131 let req: LspSendReq = serde_json::from_value(serde_json::json!({
132 "server_id": "srv-1",
133 "message": message.clone(),
134 }))
135 .unwrap_or_else(|e| panic!("{message} should deserialize, got {e}"));
136 assert_eq!(req.message, message, "the proxy must not reshape the payload");
137 }
138 }
139
140 #[tokio::test]
145 async fn a_malformed_request_is_rejected_rather_than_run() {
146 let state = crate::server::tests::test_state();
147 let (engine, mut rx) = WshRpcEngine::new();
148 register_lsp_handlers(&engine, &state);
149
150 engine.handle_message(RpcMessage {
151 command: "lspstop".to_string(),
152 reqid: "req-1".to_string(),
153 data: Some(serde_json::json!({})),
154 ..Default::default()
155 });
156 let resp = tokio::time::timeout(std::time::Duration::from_secs(2), rx.recv())
157 .await
158 .unwrap()
159 .unwrap();
160 assert!(
161 resp.error.contains("server_id"),
162 "expected a missing-field error naming the field, got {:?}",
163 resp.error,
164 );
165 }
166}