diff options
author | Asher Gomez <ashersaupingomez@gmail.com> | 2024-02-20 00:34:24 +1100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-19 06:34:24 -0700 |
commit | 2b279ad630651e973d5a31586f58809f005bc925 (patch) | |
tree | 3e3cbeb4126643c75381dd5422e8603a7488bb8a /tests/util/server/src/lsp.rs | |
parent | eb542bc185c6c4ce1847417a2dfdf04862cd86db (diff) |
chore: move `test_util` to `tests/util/server` (#22444)
As discussed with @mmastrac.
---------
Signed-off-by: Asher Gomez <ashersaupingomez@gmail.com>
Co-authored-by: Matt Mastracci <matthew@mastracci.com>
Diffstat (limited to 'tests/util/server/src/lsp.rs')
-rw-r--r-- | tests/util/server/src/lsp.rs | 1104 |
1 files changed, 1104 insertions, 0 deletions
diff --git a/tests/util/server/src/lsp.rs b/tests/util/server/src/lsp.rs new file mode 100644 index 000000000..6b8256fc1 --- /dev/null +++ b/tests/util/server/src/lsp.rs @@ -0,0 +1,1104 @@ +// 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::HashSet; +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<Regex> = + lazy_regex::lazy_regex!(r"(?i)^content-length:\s+(\d+)"); + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct LspResponseError { + code: i32, + message: String, + data: Option<Value>, +} + +#[derive(Clone, Debug)] +pub enum LspMessage { + Notification(String, Option<Value>), + Request(u64, String, Option<Value>), + Response(u64, Option<Value>, Option<LspResponseError>), +} + +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<LspResponseError> = 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<R>(reader: &mut R) -> Result<Option<Vec<u8>>> +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::<usize>()?; + } + 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<Vec<LspMessage>>, Condvar)>, + read_messages: Vec<LspMessage>, +} + +impl LspStdoutReader { + pub fn new(mut buf_reader: io::BufReader<ChildStdout>) -> Self { + let messages: Arc<(Mutex<Vec<LspMessage>>, 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<R>( + &mut self, + mut get_match: impl FnMut(&LspMessage) -> Option<R>, + ) -> 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<R>( + &mut self, + mut get_match: impl FnMut(&LspMessage) -> Option<R>, + ) -> 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<Url>) -> &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<lsp_types::WorkspaceFolder>, + ) -> &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<str>) -> &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<serde_json::Value>, + ) -> &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<str>) -> &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<String>) -> &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<String>) -> &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<str>) -> &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<str>) -> &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<String, serde_json::Value> { + 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, + deno_exe: PathRef, + root_dir: PathRef, + use_diagnostic_sync: bool, + deno_dir: TempDir, +} + +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, + deno_exe: deno_exe_path(), + root_dir: deno_dir.path().clone(), + use_diagnostic_sync: true, + deno_dir, + } + } + + pub fn deno_exe(mut self, exe_path: impl AsRef<Path>) -> 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 + } + + /// 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 build(&self) -> LspClient { + self.build_result().unwrap() + } + + pub fn build_result(&self) -> Result<LspClient> { + let deno_dir = self.deno_dir.clone(); + let mut command = Command::new(&self.deno_exe); + 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") + .arg("lsp") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()); + 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::<String>(); + 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<ChildStdin>, + deno_dir: TempDir, + root_dir: PathRef, + stderr_lines_rx: Option<mpsc::Receiver<String>>, + 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, condition: impl Fn(&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::<Value>(); + 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. It's assumed that + 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::<DiagnosticBatchNotificationParams>(); + 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::<lsp::PublishDiagnosticsParams>(); + 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<R>(&mut self) -> (String, Option<R>) + 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<R>(&mut self) -> (String, Option<R>) + 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<R>( + &mut self, + expected_method: &str, + ) -> Option<R> + 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<R>(&mut self) -> (u64, String, Option<R>) + 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, + }) + } + + 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<str>, + position: (usize, usize), + context: Value, + ) -> lsp::CompletionResponse { + self.write_request_with_res_as::<lsp::CompletionResponse>( + "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<str>, + 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<R>( + &mut self, + method: impl AsRef<str>, + 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<str>, + 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 write_response<V>(&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<S, V>(&mut self, method: S, params: V) + where + S: AsRef<str>, + V: Serialize, + { + let value = json!({ + "jsonrpc": "2.0", + "method": method.as_ref(), + "params": params, + }); + self.write(value); + } +} + +#[derive(Debug, Clone)] +pub struct CollectedDiagnostics(Vec<lsp::PublishDiagnosticsParams>); + +impl CollectedDiagnostics { + /// Gets the diagnostics that the editor will see after all the publishes. + pub fn all(&self) -> Vec<lsp::Diagnostic> { + 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<lsp::PublishDiagnosticsParams> { + 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(); + } +} |