1use std::io::{BufRead, BufReader, Read, Write};
6use std::process::{Child, ChildStdin, ChildStdout};
7
8use serde_json::{Value, json};
9
10pub struct LspTransport {
12 stdin: ChildStdin,
14 reader: BufReader<ChildStdout>,
15}
16
17impl LspTransport {
18 pub fn new(child: &mut Child) -> Self {
26 let stdin = child.stdin.take().expect("stdin");
27 let stdout = child.stdout.take().expect("stdout");
28 Self {
29 stdin,
30 reader: BufReader::new(stdout),
31 }
32 }
33
34 pub fn send(&mut self, msg: &Value) -> std::io::Result<()> {
40 let body = msg.to_string();
41 write!(self.stdin, "Content-Length: {}\r\n\r\n{}", body.len(), body)?;
42 self.stdin.flush()
43 }
44
45 pub fn recv(&mut self) -> anyhow::Result<Value> {
55 let mut content_length: Option<usize> = None;
57 loop {
58 let mut line = String::new();
59 self.reader.read_line(&mut line)?;
60 let line = line.trim_end_matches(['\r', '\n']);
61 if line.is_empty() {
62 break;
63 }
64 if let Some(val) = line.strip_prefix("Content-Length: ") {
65 content_length = Some(val.trim().parse()?);
66 }
67 }
68
69 let length = content_length.ok_or_else(|| anyhow::anyhow!("No Content-Length header"))?;
70 let mut body = vec![0u8; length];
71 self.reader.read_exact(&mut body)?;
72 Ok(serde_json::from_slice(&body)?)
73 }
74
75 pub fn recv_until<T>(
83 &mut self,
84 limit: usize,
85 mut predicate: impl FnMut(&Value) -> Option<T>,
86 ) -> anyhow::Result<T> {
87 for _ in 0..limit {
88 let msg = self.recv()?;
89 if let Some(result) = predicate(&msg) {
90 return Ok(result);
91 }
92 }
93 anyhow::bail!("recv_until: exhausted {limit} messages without a match")
94 }
95
96 #[must_use]
100 pub fn initialize(process_id: u32, root_uri: &str) -> Value {
101 json!({
102 "jsonrpc": "2.0",
103 "id": 1,
104 "method": "initialize",
105 "params": {
106 "processId": process_id,
107 "rootUri": root_uri,
108 "capabilities": {
109 "textDocument": {
110 "completion": {
111 "completionItem": { "snippetSupport": false }
112 }
113 }
114 },
115 "initializationOptions": {
116 "procMacro": { "enable": false }
118 }
119 }
120 })
121 }
122
123 #[must_use]
125 pub fn initialized() -> Value {
126 json!({ "jsonrpc": "2.0", "method": "initialized", "params": {} })
127 }
128
129 #[must_use]
131 pub fn did_open(uri: &str, text: &str) -> Value {
132 json!({
133 "jsonrpc": "2.0",
134 "method": "textDocument/didOpen",
135 "params": {
136 "textDocument": {
137 "uri": uri,
138 "languageId": "rust",
139 "version": 1,
140 "text": text
141 }
142 }
143 })
144 }
145
146 #[must_use]
148 pub fn definition(id: u64, uri: &str, line: u32, character: u32) -> Value {
149 json!({
150 "jsonrpc": "2.0",
151 "id": id,
152 "method": "textDocument/definition",
153 "params": {
154 "textDocument": { "uri": uri },
155 "position": { "line": line, "character": character }
156 }
157 })
158 }
159
160 #[must_use]
162 pub fn completion(id: u64, uri: &str, line: u32, character: u32) -> Value {
163 json!({
164 "jsonrpc": "2.0",
165 "id": id,
166 "method": "textDocument/completion",
167 "params": {
168 "textDocument": { "uri": uri },
169 "position": { "line": line, "character": character },
170 "context": { "triggerKind": 2, "triggerCharacter": "." }
171 }
172 })
173 }
174
175 #[must_use]
177 pub fn shutdown(id: u64) -> Value {
178 json!({ "jsonrpc": "2.0", "id": id, "method": "shutdown", "params": null })
179 }
180
181 #[must_use]
183 pub fn exit() -> Value {
184 json!({ "jsonrpc": "2.0", "method": "exit", "params": null })
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
197 fn test_initialize_message_structure() {
198 let msg = LspTransport::initialize(12345, "file:///test");
199
200 assert_eq!(msg["jsonrpc"], "2.0");
201 assert_eq!(msg["id"], 1);
202 assert_eq!(msg["method"], "initialize");
203 assert_eq!(msg["params"]["processId"], 12345);
204 assert_eq!(msg["params"]["rootUri"], "file:///test");
205 }
206
207 #[test]
208 fn test_completion_message_structure() {
209 let msg = LspTransport::completion(42, "file:///test.rs", 10, 5);
210
211 assert_eq!(msg["jsonrpc"], "2.0");
212 assert_eq!(msg["id"], 42);
213 assert_eq!(msg["method"], "textDocument/completion");
214 assert_eq!(msg["params"]["position"]["line"], 10);
215 assert_eq!(msg["params"]["position"]["character"], 5);
216 assert_eq!(msg["params"]["context"]["triggerKind"], 2);
217 assert_eq!(msg["params"]["context"]["triggerCharacter"], ".");
218 }
219
220 #[test]
221 fn test_definition_message_structure() {
222 let msg = LspTransport::definition(99, "file:///main.rs", 20, 15);
223
224 assert_eq!(msg["jsonrpc"], "2.0");
225 assert_eq!(msg["id"], 99);
226 assert_eq!(msg["method"], "textDocument/definition");
227 assert_eq!(msg["params"]["textDocument"]["uri"], "file:///main.rs");
228 assert_eq!(msg["params"]["position"]["line"], 20);
229 assert_eq!(msg["params"]["position"]["character"], 15);
230 }
231
232 #[test]
233 fn test_shutdown_and_exit() {
234 let shutdown = LspTransport::shutdown(100);
235 assert_eq!(shutdown["method"], "shutdown");
236 assert_eq!(shutdown["id"], 100);
237
238 let exit = LspTransport::exit();
239 assert_eq!(exit["method"], "exit");
240 assert!(exit["id"].is_null());
241 }
242}