char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somexperiment: use native typescript lsp supportc7a9569

main
14.7 KiB460 linesraw
1// Synchronous LSP client harness for integration tests, replacing the old
2// TypeScript test driver. Tests that need a native TypeScript backend set
3// AVRIL_TEST_TYPESCRIPT to its absolute path and are skipped otherwise.
4use std::fs;
5use std::io::{BufRead, BufReader, Read, Write};
6use std::path::{Path, PathBuf};
7use std::process::{Child, ChildStdin, Command, ExitStatus, Stdio};
8use std::sync::mpsc::{Receiver, RecvTimeoutError, channel};
9use std::sync::{Arc, Mutex};
10use std::time::{Duration, Instant};
11
12use serde_json::{Value, json};
13
14pub fn backend() -> Option<PathBuf> {
15    let path = std::env::var_os("AVRIL_TEST_TYPESCRIPT")?;
16    if path.is_empty() {
17        return None;
18    }
19    Some(PathBuf::from(path))
20}
21
22pub fn avril() -> PathBuf {
23    PathBuf::from(env!("CARGO_BIN_EXE_avril"))
24}
25
26pub fn write_frame(writer: &mut impl Write, message: &Value) -> std::io::Result<()> {
27    let body = serde_json::to_vec(message).unwrap();
28    write!(writer, "Content-Length: {}\r\n\r\n", body.len())?;
29    writer.write_all(&body)?;
30    writer.flush()
31}
32
33pub fn read_frame(reader: &mut impl BufRead) -> std::io::Result<Option<Value>> {
34    let mut length = None;
35    loop {
36        let mut line = String::new();
37        if reader.read_line(&mut line)? == 0 {
38            return Ok(None);
39        }
40        let line = line.trim_end_matches(['\r', '\n']);
41        if line.is_empty() {
42            break;
43        }
44        if let Some((name, value)) = line.split_once(':')
45            && name.trim().eq_ignore_ascii_case("Content-Length")
46        {
47            length = Some(value.trim().parse::<usize>().map_err(|_| {
48                std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid Content-Length")
49            })?);
50        }
51    }
52    let length = length.ok_or_else(|| {
53        std::io::Error::new(std::io::ErrorKind::InvalidData, "Missing Content-Length")
54    })?;
55    if length > 64 * 1024 * 1024 {
56        return Err(std::io::Error::new(
57            std::io::ErrorKind::InvalidData,
58            "RPC frame exceeds test transport limit",
59        ));
60    }
61    let mut body = vec![0; length];
62    reader.read_exact(&mut body)?;
63    Ok(Some(serde_json::from_slice(&body).unwrap()))
64}
65
66#[derive(Debug)]
67pub struct RpcError {
68    pub code: i64,
69    pub message: String,
70}
71
72impl std::fmt::Display for RpcError {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        write!(f, "RPC {}: {}", self.code, self.message)
75    }
76}
77
78pub struct Server {
79    child: Child,
80    stdin: Option<ChildStdin>,
81    messages: Receiver<Value>,
82    pub notifications: Vec<Value>,
83    next_id: u64,
84    root: PathBuf,
85    stderr: Arc<Mutex<Vec<u8>>>,
86}
87
88impl Server {
89    pub fn start(root: &Path, backend: &Path, options: &[&str]) -> Server {
90        let mut child = Command::new(avril())
91            .current_dir(root)
92            .args(["lsp", &format!("--typescript={}", backend.display())])
93            .arg(format!("--project={}", root.display()))
94            .args(options)
95            .stdin(Stdio::piped())
96            .stdout(Stdio::piped())
97            .stderr(Stdio::piped())
98            .spawn()
99            .unwrap();
100        let stdin = child.stdin.take().unwrap();
101        let mut stdout = BufReader::new(child.stdout.take().unwrap());
102        let (tx, messages) = channel();
103        std::thread::spawn(move || {
104            while let Ok(Some(message)) = read_frame(&mut stdout) {
105                if tx.send(message).is_err() {
106                    return;
107                }
108            }
109        });
110        let stderr = Arc::new(Mutex::new(Vec::new()));
111        let mut pipe = child.stderr.take().unwrap();
112        std::thread::spawn({
113            let stderr = stderr.clone();
114            move || {
115                let mut chunk = [0; 8192];
116                while let Ok(count) = pipe.read(&mut chunk) {
117                    if count == 0 {
118                        break;
119                    }
120                    stderr.lock().unwrap().extend_from_slice(&chunk[..count]);
121                }
122            }
123        });
124        Server {
125            child,
126            stdin: Some(stdin),
127            messages,
128            notifications: Vec::new(),
129            next_id: 0,
130            root: root.to_owned(),
131            stderr,
132        }
133    }
134
135    pub fn pid(&self) -> u32 {
136        self.child.id()
137    }
138
139    pub fn stderr(&self) -> String {
140        String::from_utf8_lossy(&self.stderr.lock().unwrap()).into_owned()
141    }
142
143    fn send(&mut self, message: &Value) {
144        write_frame(self.stdin.as_mut().unwrap(), message).unwrap();
145    }
146
147    pub fn notify(&mut self, method: &str, params: Value) {
148        self.send(&json!({"jsonrpc": "2.0", "method": method, "params": params}));
149    }
150
151    /// Sends a request without waiting, for tests that must act (cancel,
152    /// signal, close) while it is in flight.
153    pub fn request_start(&mut self, method: &str, params: Value) -> u64 {
154        self.next_id += 1;
155        let id = self.next_id;
156        self.send(&json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params}));
157        id
158    }
159
160    pub fn request_wait(&mut self, id: u64) -> Value {
161        let deadline = Instant::now() + Duration::from_secs(15);
162        loop {
163            let remaining = deadline.saturating_duration_since(Instant::now());
164            assert!(
165                !remaining.is_zero(),
166                "Timeout waiting for response {id}\n{}",
167                self.stderr()
168            );
169            match self.messages.recv_timeout(remaining) {
170                Ok(message) => {
171                    if message.get("method").is_some() {
172                        self.notifications.push(message);
173                    } else if message["id"].as_u64() == Some(id) {
174                        return message;
175                    }
176                }
177                Err(RecvTimeoutError::Timeout) => {
178                    panic!("Timeout waiting for response {id}\n{}", self.stderr())
179                }
180                Err(RecvTimeoutError::Disconnected) => {
181                    panic!(
182                        "Unexpected LSP EOF waiting for response {id}\n{}",
183                        self.stderr()
184                    )
185                }
186            }
187        }
188    }
189
190    /// Sends a request and returns the full response message, so tests can
191    /// inspect either `result` or `error`.
192    pub fn request(&mut self, method: &str, params: Value) -> Value {
193        let id = self.request_start(method, params);
194        self.request_wait(id)
195    }
196
197    pub fn call(&mut self, method: &str, params: Value) -> Result<Value, RpcError> {
198        let response = self.request(method, params);
199        if let Some(error) = response.get("error") {
200            Err(RpcError {
201                code: error["code"].as_i64().unwrap_or_default(),
202                message: error["message"].as_str().unwrap_or_default().to_owned(),
203            })
204        } else {
205            Ok(response["result"].clone())
206        }
207    }
208
209    pub fn write_raw(&mut self, bytes: &[u8]) {
210        let stdin = self.stdin.as_mut().unwrap();
211        stdin.write_all(bytes).unwrap();
212        stdin.flush().unwrap();
213    }
214
215    pub fn close_stdin(&mut self) {
216        self.stdin.take();
217    }
218
219    pub fn wait_status(&mut self, timeout: Duration) -> ExitStatus {
220        let deadline = Instant::now() + timeout;
221        loop {
222            if let Some(status) = self.child.try_wait().unwrap() {
223                return status;
224            }
225            assert!(
226                Instant::now() < deadline,
227                "Server did not exit\n{}",
228                self.stderr()
229            );
230            std::thread::sleep(Duration::from_millis(10));
231        }
232    }
233
234    pub fn kill(&mut self) {
235        let _ = self.child.kill();
236        let _ = self.child.wait();
237    }
238
239    pub fn uri(&self, path: &str) -> String {
240        url::Url::from_file_path(self.root.join(path))
241            .unwrap()
242            .to_string()
243    }
244
245    pub fn initialize(&mut self) {
246        self.initialize_with(json!({"textDocument": {"definition": {"linkSupport": true}}}));
247    }
248
249    pub fn initialize_with(&mut self, capabilities: Value) -> Value {
250        self.initialize_params(json!({"capabilities": capabilities}))
251    }
252
253    pub fn initialize_params(&mut self, params: Value) -> Value {
254        let result = self.call("initialize", params).unwrap();
255        self.notify("initialized", json!({}));
256        result
257    }
258
259    pub fn open(&mut self, path: &str, text: &str) {
260        self.open_as(path, text, 1, "typescript");
261    }
262
263    pub fn open_version(&mut self, path: &str, text: &str, version: i64) {
264        self.open_as(path, text, version, "typescript");
265    }
266
267    pub fn open_as(&mut self, path: &str, text: &str, version: i64, language: &str) {
268        self.notify(
269            "textDocument/didOpen",
270            json!({
271                "textDocument": {
272                    "uri": self.uri(path), "text": text, "version": version,
273                    "languageId": language
274                }
275            }),
276        );
277    }
278
279    pub fn change(&mut self, path: &str, text: &str, version: i64) {
280        self.notify(
281            "textDocument/didChange",
282            json!({
283                "textDocument": {"uri": self.uri(path), "version": version},
284                "contentChanges": [{"text": text}]
285            }),
286        );
287    }
288
289    fn query(&self, path: &str, text: &str, word: &str) -> Value {
290        Self::query_uri(&self.uri(path), text, word)
291    }
292
293    fn query_uri(uri: &str, text: &str, word: &str) -> Value {
294        let offset = text.rfind(word).unwrap() + 1;
295        json!({
296            "textDocument": {"uri": uri},
297            "position": position_at(text, offset)
298        })
299    }
300
301    pub fn hover_uri(&mut self, uri: &str, text: &str, word: &str) -> String {
302        let result = self
303            .call("textDocument/hover", Self::query_uri(uri, text, word))
304            .unwrap();
305        hover_text(&result)
306    }
307
308    pub fn hover(&mut self, path: &str, text: &str, word: &str) -> String {
309        let result = self
310            .call("textDocument/hover", self.query(path, text, word))
311            .unwrap();
312        hover_text(&result)
313    }
314
315    pub fn definitions(&mut self, path: &str, text: &str, word: &str) -> Vec<Value> {
316        self.call("textDocument/definition", self.query(path, text, word))
317            .unwrap()
318            .as_array()
319            .cloned()
320            .unwrap_or_default()
321    }
322
323    pub fn complete(
324        &mut self,
325        path: &str,
326        text: &str,
327        offset: usize,
328        trigger_character: Option<&str>,
329    ) -> Value {
330        let mut params = json!({
331            "textDocument": {"uri": self.uri(path)},
332            "position": position_at(text, offset)
333        });
334        if let Some(trigger) = trigger_character {
335            params["context"] = json!({"triggerKind": 2, "triggerCharacter": trigger});
336        }
337        self.call("textDocument/completion", params).unwrap()
338    }
339
340    /// The most recently published diagnostics for a project-relative path.
341    pub fn diagnostics(&self, path: &str) -> Vec<Value> {
342        self.diagnostics_uri(&self.uri(path))
343    }
344
345    pub fn diagnostics_uri(&self, uri: &str) -> Vec<Value> {
346        self.notifications
347            .iter()
348            .rfind(|message| {
349                message["method"] == "textDocument/publishDiagnostics"
350                    && message["params"]["uri"] == uri
351            })
352            .map(|message| {
353                message["params"]["diagnostics"]
354                    .as_array()
355                    .cloned()
356                    .unwrap_or_default()
357            })
358            .unwrap_or_default()
359    }
360
361    pub fn eventually(&mut self, mut check: impl FnMut(&mut Server) -> bool) {
362        for _ in 0..80 {
363            if check(self) {
364                return;
365            }
366            std::thread::sleep(Duration::from_millis(100));
367        }
368        panic!("Live project did not update\n{}", self.stderr());
369    }
370
371    pub fn close(&mut self) {
372        self.call("shutdown", json!({})).unwrap();
373        self.notify("exit", json!({}));
374        self.close_stdin();
375        let status = self.wait_status(Duration::from_secs(8));
376        assert!(status.success(), "Server exit {status}\n{}", self.stderr());
377    }
378}
379
380impl Drop for Server {
381    fn drop(&mut self) {
382        let _ = self.child.kill();
383        let _ = self.child.wait();
384    }
385}
386
387pub fn files(root: &Path, entries: &[(&str, &str)]) {
388    for (name, text) in entries {
389        let path = root.join(name);
390        fs::create_dir_all(path.parent().unwrap()).unwrap();
391        fs::write(path, text).unwrap();
392    }
393}
394
395/// LSP position (UTF-16 code units) for a byte offset into `text`.
396pub fn position_at(text: &str, byte_offset: usize) -> Value {
397    assert!(text.is_char_boundary(byte_offset));
398    let prefix = &text[..byte_offset];
399    let line = prefix.matches(['\n', '\r']).count() - prefix.matches("\r\n").count();
400    let line_start = prefix
401        .rfind(['\n', '\r'])
402        .map(|index| index + prefix[index..].chars().next().unwrap().len_utf8())
403        .unwrap_or(0);
404    let character = text[line_start..byte_offset].encode_utf16().count();
405    json!({"line": line, "character": character})
406}
407
408/// Byte offset for an LSP position in `text`, clamping to the line end.
409pub fn offset_at(text: &str, position: &Value) -> usize {
410    let line = position["line"].as_u64().unwrap() as usize;
411    let character = position["character"].as_u64().unwrap() as usize;
412    let mut offset = 0;
413    for (current, part) in text.split_inclusive('\n').enumerate() {
414        let content = part.trim_end_matches(['\r', '\n']);
415        if current == line {
416            let mut units = 0;
417            for (index, ch) in content.char_indices() {
418                if units >= character {
419                    return offset + index;
420                }
421                units += ch.len_utf16();
422            }
423            return offset + content.len();
424        }
425        offset += part.len();
426    }
427    text.len()
428}
429
430/// The source text covered by an LSP range value.
431pub fn selected(text: &str, range: &Value) -> String {
432    text[offset_at(text, &range["start"])..offset_at(text, &range["end"])].to_owned()
433}
434
435pub fn hover_text(hover: &Value) -> String {
436    fn append(content: &Value, text: &mut String) {
437        match content {
438            Value::String(value) => text.push_str(value),
439            Value::Array(contents) => {
440                for (index, content) in contents.iter().enumerate() {
441                    if index > 0 {
442                        text.push('\n');
443                    }
444                    append(content, text);
445                }
446            }
447            Value::Object(object) => {
448                if let Some(value) = object.get("value").and_then(Value::as_str) {
449                    text.push_str(value);
450                }
451            }
452            _ => {}
453        }
454    }
455    let mut text = String::new();
456    if let Some(contents) = hover.get("contents") {
457        append(contents, &mut text);
458    }
459    text
460}