// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use crate::deno_exe_path; use crate::jsr_registry_url; use crate::npm_registry_url; use crate::PathRef; use super::TempDir; use anyhow::Result; use lsp_types as lsp; use lsp_types::ClientCapabilities; use lsp_types::ClientInfo; use lsp_types::CodeActionCapabilityResolveSupport; use lsp_types::CodeActionClientCapabilities; use lsp_types::CodeActionKindLiteralSupport; use lsp_types::CodeActionLiteralSupport; use lsp_types::CompletionClientCapabilities; use lsp_types::CompletionItemCapability; use lsp_types::FoldingRangeClientCapabilities; use lsp_types::InitializeParams; use lsp_types::TextDocumentClientCapabilities; use lsp_types::TextDocumentSyncClientCapabilities; use lsp_types::Url; use lsp_types::WorkspaceClientCapabilities; use once_cell::sync::Lazy; use parking_lot::Condvar; use parking_lot::Mutex; use regex::Regex; use serde::de; use serde::Deserialize; use serde::Serialize; use serde_json::json; use serde_json::to_value; use serde_json::Value; use std::collections::HashMap; use std::collections::HashSet; use std::ffi::OsStr; use std::ffi::OsString; use std::io; use std::io::BufRead; use std::io::BufReader; use std::io::Write; use std::path::Path; use std::process::Child; use std::process::ChildStdin; use std::process::ChildStdout; use std::process::Command; use std::process::Stdio; use std::sync::mpsc; use std::sync::Arc; use std::time::Duration; use std::time::Instant; static CONTENT_TYPE_REG: Lazy = lazy_regex::lazy_regex!(r"(?i)^content-length:\s+(\d+)"); #[derive(Clone, Debug, Deserialize, Serialize)] pub struct LspResponseError { code: i32, message: String, data: Option, } #[derive(Clone, Debug)] pub enum LspMessage { Notification(String, Option), Request(u64, String, Option), Response(u64, Option, Option), } impl<'a> From<&'a [u8]> for LspMessage { fn from(s: &'a [u8]) -> Self { let value: Value = serde_json::from_slice(s).unwrap(); let obj = value.as_object().unwrap(); if obj.contains_key("id") && obj.contains_key("method") { let id = obj.get("id").unwrap().as_u64().unwrap(); let method = obj.get("method").unwrap().as_str().unwrap().to_string(); Self::Request(id, method, obj.get("params").cloned()) } else if obj.contains_key("id") { let id = obj.get("id").unwrap().as_u64().unwrap(); let maybe_error: Option = obj .get("error") .map(|v| serde_json::from_value(v.clone()).unwrap()); Self::Response(id, obj.get("result").cloned(), maybe_error) } else { assert!(obj.contains_key("method")); let method = obj.get("method").unwrap().as_str().unwrap().to_string(); Self::Notification(method, obj.get("params").cloned()) } } } #[derive(Debug, Deserialize)] struct DiagnosticBatchNotificationParams { batch_index: usize, messages_len: usize, } fn read_message(reader: &mut R) -> Result>> where R: io::Read + io::BufRead, { let mut content_length = 0_usize; loop { let mut buf = String::new(); if reader.read_line(&mut buf)? == 0 { return Ok(None); } if let Some(captures) = CONTENT_TYPE_REG.captures(&buf) { let content_length_match = captures .get(1) .ok_or_else(|| anyhow::anyhow!("missing capture"))?; content_length = content_length_match.as_str().parse::()?; } if &buf == "\r\n" { break; } } let mut msg_buf = vec![0_u8; content_length]; reader.read_exact(&mut msg_buf)?; Ok(Some(msg_buf)) } struct LspStdoutReader { pending_messages: Arc<(Mutex>, Condvar)>, read_messages: Vec, } impl LspStdoutReader { pub fn new(mut buf_reader: io::BufReader) -> Self { let messages: Arc<(Mutex>, Condvar)> = Default::default(); std::thread::spawn({ let messages = messages.clone(); move || { while let Ok(Some(msg_buf)) = read_message(&mut buf_reader) { let msg = LspMessage::from(msg_buf.as_slice()); let cvar = &messages.1; { let mut messages = messages.0.lock(); messages.push(msg); } cvar.notify_all(); } } }); LspStdoutReader { pending_messages: messages, read_messages: Vec::new(), } } pub fn pending_len(&self) -> usize { self.pending_messages.0.lock().len() } pub fn output_pending_messages(&self) { let messages = self.pending_messages.0.lock(); eprintln!("{:?}", messages); } pub fn had_message(&self, is_match: impl Fn(&LspMessage) -> bool) -> bool { self.read_messages.iter().any(&is_match) || self.pending_messages.0.lock().iter().any(&is_match) } pub fn read_message( &mut self, mut get_match: impl FnMut(&LspMessage) -> Option, ) -> R { let (msg_queue, cvar) = &*self.pending_messages; let mut msg_queue = msg_queue.lock(); loop { for i in 0..msg_queue.len() { let msg = &msg_queue[i]; if let Some(result) = get_match(msg) { let msg = msg_queue.remove(i); self.read_messages.push(msg); return result; } } cvar.wait(&mut msg_queue); } } pub fn read_latest_message( &mut self, mut get_match: impl FnMut(&LspMessage) -> Option, ) -> R { let (msg_queue, cvar) = &*self.pending_messages; let mut msg_queue = msg_queue.lock(); loop { for i in (0..msg_queue.len()).rev() { let msg = &msg_queue[i]; if let Some(result) = get_match(msg) { let msg = msg_queue.remove(i); self.read_messages.push(msg); return result; } } cvar.wait(&mut msg_queue); } } } pub struct InitializeParamsBuilder { params: InitializeParams, } impl InitializeParamsBuilder { #[allow(clippy::new_without_default)] pub fn new(config: Value) -> Self { let mut config_as_options = json!({}); if let Some(object) = config.as_object() { if let Some(deno) = object.get("deno") { if let Some(deno) = deno.as_object() { config_as_options = json!(deno.clone()); } } let config_as_options = config_as_options.as_object_mut().unwrap(); if let Some(typescript) = object.get("typescript") { config_as_options.insert("typescript".to_string(), typescript.clone()); } if let Some(javascript) = object.get("javascript") { config_as_options.insert("javascript".to_string(), javascript.clone()); } } Self { params: InitializeParams { process_id: None, client_info: Some(ClientInfo { name: "test-harness".to_string(), version: Some("1.0.0".to_string()), }), root_uri: None, initialization_options: Some(config_as_options), capabilities: ClientCapabilities { text_document: Some(TextDocumentClientCapabilities { code_action: Some(CodeActionClientCapabilities { code_action_literal_support: Some(CodeActionLiteralSupport { code_action_kind: CodeActionKindLiteralSupport { value_set: vec![ "quickfix".to_string(), "refactor".to_string(), ], }, }), is_preferred_support: Some(true), data_support: Some(true), disabled_support: Some(true), resolve_support: Some(CodeActionCapabilityResolveSupport { properties: vec!["edit".to_string()], }), ..Default::default() }), completion: Some(CompletionClientCapabilities { completion_item: Some(CompletionItemCapability { snippet_support: Some(true), ..Default::default() }), ..Default::default() }), folding_range: Some(FoldingRangeClientCapabilities { line_folding_only: Some(true), ..Default::default() }), synchronization: Some(TextDocumentSyncClientCapabilities { dynamic_registration: Some(true), will_save: Some(true), will_save_wait_until: Some(true), did_save: Some(true), }), ..Default::default() }), workspace: Some(WorkspaceClientCapabilities { configuration: Some(true), workspace_folders: Some(true), ..Default::default() }), experimental: Some(json!({ "testingApi": true })), ..Default::default() }, ..Default::default() }, } } pub fn set_maybe_root_uri(&mut self, value: Option) -> &mut Self { self.params.root_uri = value; self } pub fn set_root_uri(&mut self, value: Url) -> &mut Self { self.set_maybe_root_uri(Some(value)) } pub fn set_workspace_folders( &mut self, folders: Vec, ) -> &mut Self { self.params.workspace_folders = Some(folders); self } pub fn enable_inlay_hints(&mut self) -> &mut Self { let options = self.initialization_options_mut(); options.insert( "inlayHints".to_string(), json!({ "parameterNames": { "enabled": "all" }, "parameterTypes": { "enabled": true }, "variableTypes": { "enabled": true }, "propertyDeclarationTypes": { "enabled": true }, "functionLikeReturnTypes": { "enabled": true }, "enumMemberValues": { "enabled": true } }), ); self } pub fn disable_testing_api(&mut self) -> &mut Self { let obj = self .params .capabilities .experimental .as_mut() .unwrap() .as_object_mut() .unwrap(); obj.insert("testingApi".to_string(), false.into()); let options = self.initialization_options_mut(); options.remove("testing"); self } pub fn set_cache(&mut self, value: impl AsRef) -> &mut Self { let options = self.initialization_options_mut(); options.insert("cache".to_string(), value.as_ref().to_string().into()); self } pub fn set_code_lens( &mut self, value: Option, ) -> &mut Self { let options = self.initialization_options_mut(); if let Some(value) = value { options.insert("codeLens".to_string(), value); } else { options.remove("codeLens"); } self } pub fn set_config(&mut self, value: impl AsRef) -> &mut Self { let options = self.initialization_options_mut(); options.insert("config".to_string(), value.as_ref().to_string().into()); self } pub fn set_disable_paths(&mut self, value: Vec) -> &mut Self { let options = self.initialization_options_mut(); options.insert("disablePaths".to_string(), value.into()); self } pub fn set_enable_paths(&mut self, value: Vec) -> &mut Self { let options = self.initialization_options_mut(); options.insert("enablePaths".to_string(), value.into()); self } pub fn set_deno_enable(&mut self, value: bool) -> &mut Self { let options = self.initialization_options_mut(); options.insert("enable".to_string(), value.into()); self } pub fn set_import_map(&mut self, value: impl AsRef) -> &mut Self { let options = self.initialization_options_mut(); options.insert("importMap".to_string(), value.as_ref().to_string().into()); self } pub fn set_preload_limit(&mut self, arg: usize) -> &mut Self { let options = self.initialization_options_mut(); options.insert("documentPreloadLimit".to_string(), arg.into()); self } pub fn set_tls_certificate(&mut self, value: impl AsRef) -> &mut Self { let options = self.initialization_options_mut(); options.insert( "tlsCertificate".to_string(), value.as_ref().to_string().into(), ); self } pub fn set_unstable(&mut self, value: bool) -> &mut Self { let options = self.initialization_options_mut(); options.insert("unstable".to_string(), value.into()); self } pub fn add_test_server_suggestions(&mut self) -> &mut Self { self.set_suggest_imports_hosts(vec![( "http://localhost:4545/".to_string(), true, )]) } pub fn set_suggest_imports_hosts( &mut self, values: Vec<(String, bool)>, ) -> &mut Self { let options = self.initialization_options_mut(); let suggest = options.get_mut("suggest").unwrap().as_object_mut().unwrap(); let imports = suggest.get_mut("imports").unwrap().as_object_mut().unwrap(); let hosts = imports.get_mut("hosts").unwrap().as_object_mut().unwrap(); hosts.clear(); for (key, value) in values { hosts.insert(key, value.into()); } self } pub fn with_capabilities( &mut self, mut action: impl FnMut(&mut ClientCapabilities), ) -> &mut Self { action(&mut self.params.capabilities); self } fn initialization_options_mut( &mut self, ) -> &mut serde_json::Map { let options = self.params.initialization_options.as_mut().unwrap(); options.as_object_mut().unwrap() } pub fn build(&self) -> InitializeParams { self.params.clone() } } pub struct LspClientBuilder { print_stderr: bool, capture_stderr: bool, log_debug: bool, deno_exe: PathRef, root_dir: PathRef, use_diagnostic_sync: bool, deno_dir: TempDir, envs: HashMap, } impl LspClientBuilder { #[allow(clippy::new_without_default)] pub fn new() -> Self { Self::new_with_dir(TempDir::new()) } pub fn new_with_dir(deno_dir: TempDir) -> Self { Self { print_stderr: false, capture_stderr: false, log_debug: false, deno_exe: deno_exe_path(), root_dir: deno_dir.path().clone(), use_diagnostic_sync: true, deno_dir, envs: Default::default(), } } pub fn deno_exe(mut self, exe_path: impl AsRef) -> Self { self.deno_exe = PathRef::new(exe_path); self } // not deprecated, this is just here so you don't accidentally // commit code with this enabled #[deprecated] pub fn print_stderr(mut self) -> Self { self.print_stderr = true; self } pub fn capture_stderr(mut self) -> Self { self.capture_stderr = true; self } pub fn log_debug(mut self) -> Self { self.log_debug = true; self } /// Whether to use the synchronization messages to better sync diagnostics /// between the test client and server. pub fn use_diagnostic_sync(mut self, value: bool) -> Self { self.use_diagnostic_sync = value; self } pub fn set_root_dir(mut self, root_dir: PathRef) -> Self { self.root_dir = root_dir; self } pub fn env( mut self, key: impl AsRef, value: impl AsRef, ) -> Self { self .envs .insert(key.as_ref().to_owned(), value.as_ref().to_owned()); self } pub fn build(&self) -> LspClient { self.build_result().unwrap() } pub fn build_result(&self) -> Result { let deno_dir = self.deno_dir.clone(); let mut command = Command::new(&self.deno_exe); let mut args = vec!["lsp".to_string()]; if self.log_debug { args.push("--log-level=debug".to_string()); } command .env("DENO_DIR", deno_dir.path()) .env("NPM_CONFIG_REGISTRY", npm_registry_url()) .env("JSR_URL", jsr_registry_url()) // turn on diagnostic synchronization communication .env( "DENO_DONT_USE_INTERNAL_LSP_DIAGNOSTIC_SYNC_FLAG", if self.use_diagnostic_sync { "1" } else { "" }, ) .env("DENO_NO_UPDATE_CHECK", "1") .args(args) .stdin(Stdio::piped()) .stdout(Stdio::piped()); for (key, value) in &self.envs { command.env(key, value); } if self.capture_stderr { command.stderr(Stdio::piped()); } else if !self.print_stderr { command.stderr(Stdio::null()); } let mut child = command.spawn()?; let stdout = child.stdout.take().unwrap(); let buf_reader = io::BufReader::new(stdout); let reader = LspStdoutReader::new(buf_reader); let stdin = child.stdin.take().unwrap(); let writer = io::BufWriter::new(stdin); let stderr_lines_rx = if self.capture_stderr { let stderr = child.stderr.take().unwrap(); let print_stderr = self.print_stderr; let (tx, rx) = mpsc::channel::(); std::thread::spawn(move || { let stderr = BufReader::new(stderr); for line in stderr.lines() { match line { Ok(line) => { if print_stderr { eprintln!("{}", line); } tx.send(line).unwrap(); } Err(err) => { panic!("failed to read line from stderr: {:#}", err); } } } }); Some(rx) } else { None }; Ok(LspClient { child, reader, request_id: 1, start: Instant::now(), root_dir: self.root_dir.clone(), writer, deno_dir, stderr_lines_rx, config: json!("{}"), supports_workspace_configuration: false, }) } } pub struct LspClient { child: Child, reader: LspStdoutReader, request_id: u64, start: Instant, writer: io::BufWriter, deno_dir: TempDir, root_dir: PathRef, stderr_lines_rx: Option>, config: serde_json::Value, supports_workspace_configuration: bool, } impl Drop for LspClient { fn drop(&mut self) { match self.child.try_wait() { Ok(None) => { self.child.kill().unwrap(); let _ = self.child.wait(); } Ok(Some(status)) => panic!("deno lsp exited unexpectedly {status}"), Err(e) => panic!("pebble error: {e}"), } } } impl LspClient { pub fn deno_dir(&self) -> &TempDir { &self.deno_dir } pub fn duration(&self) -> Duration { self.start.elapsed() } pub fn queue_is_empty(&self) -> bool { self.reader.pending_len() == 0 } pub fn queue_len(&self) -> usize { self.reader.output_pending_messages(); self.reader.pending_len() } #[track_caller] pub fn wait_until_stderr_line( &self, mut condition: impl FnMut(&str) -> bool, ) { let timeout_time = Instant::now().checked_add(Duration::from_secs(5)).unwrap(); let lines_rx = self .stderr_lines_rx .as_ref() .expect("must setup with client_builder.capture_stderr()"); let mut found_lines = Vec::new(); while Instant::now() < timeout_time { if let Ok(line) = lines_rx.try_recv() { if condition(&line) { return; } found_lines.push(line); } std::thread::sleep(Duration::from_millis(20)); } eprintln!("==== STDERR OUTPUT ===="); for line in found_lines { eprintln!("{}", line) } eprintln!("== END STDERR OUTPUT =="); panic!("Timed out waiting on condition.") } pub fn initialize_default(&mut self) { self.initialize(|_| {}) } pub fn initialize( &mut self, do_build: impl Fn(&mut InitializeParamsBuilder), ) { self.initialize_with_config( do_build, json!({ "deno": { "enable": true, "cache": null, "certificateStores": null, "codeLens": { "implementations": true, "references": true, "test": true, }, "config": null, "importMap": null, "lint": true, "suggest": { "autoImports": true, "completeFunctionCalls": false, "names": true, "paths": true, "imports": { "hosts": {}, }, }, "testing": { "args": [ "--allow-all" ], "enable": true, }, "tlsCertificate": null, "unsafelyIgnoreCertificateErrors": null, "unstable": false, } }), ) } pub fn initialize_with_config( &mut self, do_build: impl Fn(&mut InitializeParamsBuilder), mut config: Value, ) { let mut builder = InitializeParamsBuilder::new(config.clone()); builder.set_root_uri(self.root_dir.uri_dir()); do_build(&mut builder); let params: InitializeParams = builder.build(); // `config` must be updated to account for the builder changes. // TODO(nayeemrmn): Remove config-related methods from builder. if let Some(options) = ¶ms.initialization_options { if let Some(options) = options.as_object() { if let Some(config) = config.as_object_mut() { let mut deno = options.clone(); let typescript = options.get("typescript"); let javascript = options.get("javascript"); deno.remove("typescript"); deno.remove("javascript"); config.insert("deno".to_string(), json!(deno)); if let Some(typescript) = typescript { config.insert("typescript".to_string(), typescript.clone()); } if let Some(javascript) = javascript { config.insert("javascript".to_string(), javascript.clone()); } } } } self.supports_workspace_configuration = match ¶ms.capabilities.workspace { Some(workspace) => workspace.configuration == Some(true), _ => false, }; self.write_request("initialize", params); self.write_notification("initialized", json!({})); self.config = config; if self.supports_workspace_configuration { self.handle_configuration_request(); } } pub fn did_open(&mut self, params: Value) -> CollectedDiagnostics { self.did_open_raw(params); self.read_diagnostics() } pub fn did_open_raw(&mut self, params: Value) { self.write_notification("textDocument/didOpen", params); } pub fn change_configuration(&mut self, config: Value) { self.config = config; if self.supports_workspace_configuration { self.write_notification( "workspace/didChangeConfiguration", json!({ "settings": {} }), ); self.handle_configuration_request(); } else { self.write_notification( "workspace/didChangeConfiguration", json!({ "settings": &self.config }), ); } } pub fn handle_configuration_request(&mut self) { let (id, method, args) = self.read_request::(); assert_eq!(method, "workspace/configuration"); let params = args.as_ref().unwrap().as_object().unwrap(); let items = params.get("items").unwrap().as_array().unwrap(); let config_object = self.config.as_object().unwrap(); let mut result = vec![]; for item in items { let item = item.as_object().unwrap(); let section = item.get("section").unwrap().as_str().unwrap(); result.push(config_object.get(section).cloned().unwrap_or_default()); } self.write_response(id, result); } pub fn did_save(&mut self, params: Value) { self.write_notification("textDocument/didSave", params); } pub fn did_change_watched_files(&mut self, params: Value) { self.write_notification("workspace/didChangeWatchedFiles", params); } fn get_latest_diagnostic_batch_index(&mut self) -> usize { let result = self .write_request("deno/internalLatestDiagnosticBatchIndex", json!(null)); result.as_u64().unwrap() as usize } /// Reads the latest diagnostics. pub fn read_diagnostics(&mut self) -> CollectedDiagnostics { // wait for three (deno, lint, and typescript diagnostics) batch // notification messages for that index let mut read = 0; let mut total_messages_len = 0; while read < 3 { let (method, response) = self.read_notification::(); assert_eq!(method, "deno/internalTestDiagnosticBatch"); let response = response.unwrap(); if response.batch_index == self.get_latest_diagnostic_batch_index() { read += 1; total_messages_len += response.messages_len; } } // now read the latest diagnostic messages let mut all_diagnostics = Vec::with_capacity(total_messages_len); let mut seen_files = HashSet::new(); for _ in 0..total_messages_len { let (method, response) = self.read_latest_notification::(); assert_eq!(method, "textDocument/publishDiagnostics"); let response = response.unwrap(); if seen_files.insert(response.uri.to_string()) { all_diagnostics.push(response); } } CollectedDiagnostics(all_diagnostics) } pub fn shutdown(&mut self) { self.write_request("shutdown", json!(null)); self.write_notification("exit", json!(null)); } // it's flaky to assert for a notification because a notification // might arrive a little later, so only provide a method for asserting // that there is no notification pub fn assert_no_notification(&mut self, searching_method: &str) { assert!(!self.reader.had_message(|message| match message { LspMessage::Notification(method, _) => method == searching_method, _ => false, })) } pub fn read_notification(&mut self) -> (String, Option) where R: de::DeserializeOwned, { self.reader.read_message(|msg| match msg { LspMessage::Notification(method, maybe_params) => { let params = serde_json::from_value(maybe_params.clone()?).ok()?; Some((method.to_string(), params)) } _ => None, }) } pub fn read_latest_notification(&mut self) -> (String, Option) where R: de::DeserializeOwned, { self.reader.read_latest_message(|msg| match msg { LspMessage::Notification(method, maybe_params) => { let params = serde_json::from_value(maybe_params.clone()?).ok()?; Some((method.to_string(), params)) } _ => None, }) } pub fn read_notification_with_method( &mut self, expected_method: &str, ) -> Option where R: de::DeserializeOwned, { self.reader.read_message(|msg| match msg { LspMessage::Notification(method, maybe_params) => { if method != expected_method { None } else { serde_json::from_value(maybe_params.clone()?).ok() } } _ => None, }) } pub fn read_request(&mut self) -> (u64, String, Option) where R: de::DeserializeOwned, { self.reader.read_message(|msg| match msg { LspMessage::Request(id, method, maybe_params) => Some(( *id, method.to_owned(), maybe_params .clone() .map(|p| serde_json::from_value(p).unwrap()), )), _ => None, }) } pub fn write_jsonrpc( &mut self, method: impl AsRef, params: impl Serialize, ) { let value = json!({ "jsonrpc": "2.0", "id": self.request_id, "method": method.as_ref(), "params": params, }); self.write(value); self.request_id += 1; } fn write(&mut self, value: Value) { let value_str = value.to_string(); let msg = format!( "Content-Length: {}\r\n\r\n{}", value_str.as_bytes().len(), value_str ); self.writer.write_all(msg.as_bytes()).unwrap(); self.writer.flush().unwrap(); } pub fn get_completion( &mut self, uri: impl AsRef, position: (usize, usize), context: Value, ) -> lsp::CompletionResponse { self.write_request_with_res_as::( "textDocument/completion", json!({ "textDocument": { "uri": uri.as_ref(), }, "position": { "line": position.0, "character": position.1 }, "context": context, }), ) } pub fn get_completion_list( &mut self, uri: impl AsRef, position: (usize, usize), context: Value, ) -> lsp::CompletionList { let res = self.get_completion(uri, position, context); if let lsp::CompletionResponse::List(list) = res { list } else { panic!("unexpected response"); } } pub fn write_request_with_res_as( &mut self, method: impl AsRef, params: impl Serialize, ) -> R where R: de::DeserializeOwned, { let result = self.write_request(method, params); serde_json::from_value(result).unwrap() } pub fn write_request( &mut self, method: impl AsRef, params: impl Serialize, ) -> Value { let value = if to_value(¶ms).unwrap().is_null() { json!({ "jsonrpc": "2.0", "id": self.request_id, "method": method.as_ref(), }) } else { json!({ "jsonrpc": "2.0", "id": self.request_id, "method": method.as_ref(), "params": params, }) }; self.write(value); self.reader.read_message(|msg| match msg { LspMessage::Response(id, maybe_result, maybe_error) => { assert_eq!(*id, self.request_id); self.request_id += 1; if let Some(error) = maybe_error { panic!("LSP ERROR: {error:?}"); } Some(maybe_result.clone().unwrap()) } _ => None, }) } pub fn read_latest_response( &mut self, ) -> (u64, Option, Option) { self.reader.read_message(|msg| match msg { LspMessage::Response(id, val, err) => { Some((*id, val.clone(), err.clone())) } _ => None, }) } pub fn write_response(&mut self, id: u64, result: V) where V: Serialize, { let value = json!({ "jsonrpc": "2.0", "id": id, "result": result }); self.write(value); } pub fn write_notification(&mut self, method: S, params: V) where S: AsRef, V: Serialize, { let value = json!({ "jsonrpc": "2.0", "method": method.as_ref(), "params": params, }); self.write(value); } } #[derive(Debug, Clone)] pub struct CollectedDiagnostics(Vec); impl CollectedDiagnostics { /// Gets the diagnostics that the editor will see after all the publishes. pub fn all(&self) -> Vec { self .all_messages() .into_iter() .flat_map(|m| m.diagnostics) .collect() } /// Gets the messages that the editor will see after all the publishes. pub fn all_messages(&self) -> Vec { self.0.clone() } pub fn messages_with_source( &self, source: &str, ) -> lsp::PublishDiagnosticsParams { self .all_messages() .iter() .find(|p| { p.diagnostics .iter() .any(|d| d.source == Some(source.to_string())) }) .map(ToOwned::to_owned) .unwrap() } #[track_caller] pub fn messages_with_file_and_source( &self, specifier: &str, source: &str, ) -> lsp::PublishDiagnosticsParams { let specifier = Url::parse(specifier).unwrap(); self .all_messages() .iter() .find(|p| { p.uri == specifier && p .diagnostics .iter() .any(|d| d.source == Some(source.to_string())) }) .map(ToOwned::to_owned) .unwrap() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_read_message() { let msg1 = b"content-length: 11\r\n\r\nhello world"; let mut reader1 = std::io::Cursor::new(msg1); assert_eq!(read_message(&mut reader1).unwrap().unwrap(), b"hello world"); let msg2 = b"content-length: 5\r\n\r\nhello world"; let mut reader2 = std::io::Cursor::new(msg2); assert_eq!(read_message(&mut reader2).unwrap().unwrap(), b"hello"); } #[test] #[should_panic(expected = "failed to fill whole buffer")] fn test_invalid_read_message() { let msg1 = b"content-length: 12\r\n\r\nhello world"; let mut reader1 = std::io::Cursor::new(msg1); read_message(&mut reader1).unwrap(); } }