diff options
Diffstat (limited to 'cli/tests/integration/lsp_tests.rs')
-rw-r--r-- | cli/tests/integration/lsp_tests.rs | 6434 |
1 files changed, 6434 insertions, 0 deletions
diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs new file mode 100644 index 000000000..3fba65c46 --- /dev/null +++ b/cli/tests/integration/lsp_tests.rs @@ -0,0 +1,6434 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use deno_ast::ModuleSpecifier; +use deno_core::serde::de::DeserializeOwned; +use deno_core::serde::Deserialize; +use deno_core::serde::Serialize; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::url::Url; +use pretty_assertions::assert_eq; +use std::collections::HashSet; +use std::fs; +use std::process::Stdio; +use test_util::deno_cmd_with_deno_dir; +use test_util::deno_exe_path; +use test_util::env_vars_for_npm_tests; +use test_util::http_server; +use test_util::lsp::LspClient; +use test_util::testdata_path; +use test_util::TempDir; +use tower_lsp::lsp_types as lsp; + +fn load_fixture(path: &str) -> Value { + load_fixture_as(path) +} + +fn load_fixture_as<T>(path: &str) -> T +where + T: DeserializeOwned, +{ + let fixture_str = load_fixture_str(path); + serde_json::from_str::<T>(&fixture_str).unwrap() +} + +fn load_fixture_str(path: &str) -> String { + let fixtures_path = testdata_path().join("lsp"); + let path = fixtures_path.join(path); + fs::read_to_string(path).unwrap() +} + +fn init(init_path: &str) -> LspClient { + let deno_exe = deno_exe_path(); + let mut client = LspClient::new(&deno_exe, false).unwrap(); + client + .write_request::<_, _, Value>("initialize", load_fixture(init_path)) + .unwrap(); + client.write_notification("initialized", json!({})).unwrap(); + client +} + +fn did_open<V>( + client: &mut LspClient, + params: V, +) -> Vec<lsp::PublishDiagnosticsParams> +where + V: Serialize, +{ + client + .write_notification("textDocument/didOpen", params) + .unwrap(); + + handle_configuration_request( + client, + json!([{ + "enable": true, + "codeLens": { + "test": true + } + }]), + ); + read_diagnostics(client).0 +} + +fn handle_configuration_request(client: &mut LspClient, result: Value) { + let (id, method, _) = client.read_request::<Value>().unwrap(); + assert_eq!(method, "workspace/configuration"); + client.write_response(id, result).unwrap(); +} + +fn read_diagnostics(client: &mut LspClient) -> CollectedDiagnostics { + // diagnostics come in batches of three unless they're cancelled + let mut diagnostics = vec![]; + for _ in 0..3 { + let (method, response) = client + .read_notification::<lsp::PublishDiagnosticsParams>() + .unwrap(); + assert_eq!(method, "textDocument/publishDiagnostics"); + diagnostics.push(response.unwrap()); + } + CollectedDiagnostics(diagnostics) +} + +fn shutdown(client: &mut LspClient) { + client + .write_request::<_, _, Value>("shutdown", json!(null)) + .unwrap(); + client.write_notification("exit", json!(null)).unwrap(); +} + +pub fn ensure_directory_specifier( + mut specifier: ModuleSpecifier, +) -> ModuleSpecifier { + let path = specifier.path(); + if !path.ends_with('/') { + let new_path = format!("{}/", path); + specifier.set_path(&new_path); + } + specifier +} + +struct TestSession { + client: LspClient, + open_file_count: usize, +} + +impl TestSession { + pub fn from_file(init_path: &str) -> Self { + Self::from_client(init(init_path)) + } + + pub fn from_client(client: LspClient) -> Self { + Self { + client, + open_file_count: 0, + } + } + + pub fn did_open<V>(&mut self, params: V) -> CollectedDiagnostics + where + V: Serialize, + { + self + .client + .write_notification("textDocument/didOpen", params) + .unwrap(); + + let (id, method, _) = self.client.read_request::<Value>().unwrap(); + assert_eq!(method, "workspace/configuration"); + self + .client + .write_response( + id, + json!([{ + "enable": true, + "codeLens": { + "test": true + } + }]), + ) + .unwrap(); + + self.open_file_count += 1; + self.read_diagnostics() + } + + pub fn read_diagnostics(&mut self) -> CollectedDiagnostics { + let mut all_diagnostics = Vec::new(); + for _ in 0..self.open_file_count { + all_diagnostics.extend(read_diagnostics(&mut self.client).0); + } + CollectedDiagnostics(all_diagnostics) + } + + pub fn shutdown_and_exit(&mut self) { + shutdown(&mut self.client); + } +} + +#[derive(Debug, Clone)] +struct CollectedDiagnostics(Vec<lsp::PublishDiagnosticsParams>); + +impl CollectedDiagnostics { + /// Gets the diagnostics that the editor will see after all the publishes. + pub fn viewed(&self) -> Vec<lsp::Diagnostic> { + self + .viewed_messages() + .into_iter() + .flat_map(|m| m.diagnostics) + .collect() + } + + /// Gets the messages that the editor will see after all the publishes. + pub fn viewed_messages(&self) -> Vec<lsp::PublishDiagnosticsParams> { + // go over the publishes in reverse order in order to get + // the final messages that will be shown in the editor + let mut messages = Vec::new(); + let mut had_specifier = HashSet::new(); + for message in self.0.iter().rev() { + if had_specifier.insert(message.uri.clone()) { + messages.insert(0, message.clone()); + } + } + messages + } + + pub fn with_source(&self, source: &str) -> lsp::PublishDiagnosticsParams { + self + .viewed_messages() + .iter() + .find(|p| { + p.diagnostics + .iter() + .any(|d| d.source == Some(source.to_string())) + }) + .map(ToOwned::to_owned) + .unwrap() + } + + pub fn with_file_and_source( + &self, + specifier: &str, + source: &str, + ) -> lsp::PublishDiagnosticsParams { + let specifier = ModuleSpecifier::parse(specifier).unwrap(); + self + .viewed_messages() + .iter() + .find(|p| { + p.uri == specifier + && p + .diagnostics + .iter() + .any(|d| d.source == Some(source.to_string())) + }) + .map(ToOwned::to_owned) + .unwrap() + } +} + +#[test] +fn lsp_startup_shutdown() { + let mut client = init("initialize_params.json"); + shutdown(&mut client); +} + +#[test] +fn lsp_init_tsconfig() { + let temp_dir = TempDir::new(); + let mut params: lsp::InitializeParams = + serde_json::from_value(load_fixture("initialize_params.json")).unwrap(); + let tsconfig = + serde_json::to_vec_pretty(&load_fixture("lib.tsconfig.json")).unwrap(); + fs::write(temp_dir.path().join("lib.tsconfig.json"), tsconfig).unwrap(); + + params.root_uri = Some(Url::from_file_path(temp_dir.path()).unwrap()); + if let Some(Value::Object(mut map)) = params.initialization_options { + map.insert("config".to_string(), json!("./lib.tsconfig.json")); + params.initialization_options = Some(Value::Object(map)); + } + + let deno_exe = deno_exe_path(); + let mut client = LspClient::new(&deno_exe, false).unwrap(); + client + .write_request::<_, _, Value>("initialize", params) + .unwrap(); + + client.write_notification("initialized", json!({})).unwrap(); + + let diagnostics = did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "location.pathname;\n" + } + }), + ); + + let diagnostics = diagnostics.into_iter().flat_map(|x| x.diagnostics); + assert_eq!(diagnostics.count(), 0); + + shutdown(&mut client); +} + +#[test] +fn lsp_tsconfig_types() { + let mut params: lsp::InitializeParams = + serde_json::from_value(load_fixture("initialize_params.json")).unwrap(); + let temp_dir = TempDir::new(); + let tsconfig = + serde_json::to_vec_pretty(&load_fixture("types.tsconfig.json")).unwrap(); + fs::write(temp_dir.path().join("types.tsconfig.json"), tsconfig).unwrap(); + let a_dts = load_fixture_str("a.d.ts"); + fs::write(temp_dir.path().join("a.d.ts"), a_dts).unwrap(); + + params.root_uri = Some(Url::from_file_path(temp_dir.path()).unwrap()); + if let Some(Value::Object(mut map)) = params.initialization_options { + map.insert("config".to_string(), json!("./types.tsconfig.json")); + params.initialization_options = Some(Value::Object(map)); + } + + let deno_exe = deno_exe_path(); + let mut client = LspClient::new(&deno_exe, false).unwrap(); + client + .write_request::<_, _, Value>("initialize", params) + .unwrap(); + + client.write_notification("initialized", json!({})).unwrap(); + + let diagnostics = did_open( + &mut client, + json!({ + "textDocument": { + "uri": Url::from_file_path(temp_dir.path().join("test.ts")).unwrap(), + "languageId": "typescript", + "version": 1, + "text": "console.log(a);\n" + } + }), + ); + + let diagnostics = diagnostics.into_iter().flat_map(|x| x.diagnostics); + assert_eq!(diagnostics.count(), 0); + + shutdown(&mut client); +} + +#[test] +fn lsp_tsconfig_bad_config_path() { + let mut client = init("initialize_params_bad_config_option.json"); + let (method, maybe_params) = client.read_notification().unwrap(); + assert_eq!(method, "window/showMessage"); + assert_eq!(maybe_params, Some(lsp::ShowMessageParams { + typ: lsp::MessageType::WARNING, + message: "The path to the configuration file (\"bad_tsconfig.json\") is not resolvable.".to_string() + })); + let diagnostics = did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "console.log(Deno.args);\n" + } + }), + ); + let diagnostics = diagnostics.into_iter().flat_map(|x| x.diagnostics); + assert_eq!(diagnostics.count(), 0); +} + +#[test] +fn lsp_triple_slash_types() { + let temp_dir = TempDir::new(); + let mut params: lsp::InitializeParams = + serde_json::from_value(load_fixture("initialize_params.json")).unwrap(); + let a_dts = load_fixture_str("a.d.ts"); + fs::write(temp_dir.path().join("a.d.ts"), a_dts).unwrap(); + + params.root_uri = Some(Url::from_file_path(temp_dir.path()).unwrap()); + + let deno_exe = deno_exe_path(); + let mut client = LspClient::new(&deno_exe, false).unwrap(); + client + .write_request::<_, _, Value>("initialize", params) + .unwrap(); + + client.write_notification("initialized", json!({})).unwrap(); + + let diagnostics = did_open( + &mut client, + json!({ + "textDocument": { + "uri": Url::from_file_path(temp_dir.path().join("test.ts")).unwrap(), + "languageId": "typescript", + "version": 1, + "text": "/// <reference types=\"./a.d.ts\" />\n\nconsole.log(a);\n" + } + }), + ); + + let diagnostics = diagnostics.into_iter().flat_map(|x| x.diagnostics); + assert_eq!(diagnostics.count(), 0); + + shutdown(&mut client); +} + +#[test] +fn lsp_import_map() { + let temp_dir = TempDir::new(); + let mut params: lsp::InitializeParams = + serde_json::from_value(load_fixture("initialize_params.json")).unwrap(); + let import_map = + serde_json::to_vec_pretty(&load_fixture("import-map.json")).unwrap(); + fs::write(temp_dir.path().join("import-map.json"), import_map).unwrap(); + fs::create_dir(temp_dir.path().join("lib")).unwrap(); + fs::write( + temp_dir.path().join("lib").join("b.ts"), + r#"export const b = "b";"#, + ) + .unwrap(); + + params.root_uri = Some(Url::from_file_path(temp_dir.path()).unwrap()); + if let Some(Value::Object(mut map)) = params.initialization_options { + map.insert("importMap".to_string(), json!("import-map.json")); + params.initialization_options = Some(Value::Object(map)); + } + + let deno_exe = deno_exe_path(); + let mut client = LspClient::new(&deno_exe, false).unwrap(); + client + .write_request::<_, _, Value>("initialize", params) + .unwrap(); + + client.write_notification("initialized", json!({})).unwrap(); + let uri = Url::from_file_path(temp_dir.path().join("a.ts")).unwrap(); + + let diagnostics = did_open( + &mut client, + json!({ + "textDocument": { + "uri": uri, + "languageId": "typescript", + "version": 1, + "text": "import { b } from \"/~/b.ts\";\n\nconsole.log(b);\n" + } + }), + ); + + let diagnostics = diagnostics.into_iter().flat_map(|x| x.diagnostics); + assert_eq!(diagnostics.count(), 0); + + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/hover", + json!({ + "textDocument": { + "uri": uri + }, + "position": { + "line": 2, + "character": 12 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": [ + { + "language": "typescript", + "value":"(alias) const b: \"b\"\nimport b" + }, + "" + ], + "range": { + "start": { + "line": 2, + "character": 12 + }, + "end": { + "line": 2, + "character": 13 + } + } + })) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_import_map_data_url() { + let mut client = init("initialize_params_import_map.json"); + let diagnostics = did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "import example from \"example\";\n" + } + }), + ); + + let mut diagnostics = diagnostics.into_iter().flat_map(|x| x.diagnostics); + // This indicates that the import map from initialize_params_import_map.json + // is applied correctly. + assert!(diagnostics.any(|diagnostic| diagnostic.code + == Some(lsp::NumberOrString::String("no-cache".to_string())) + && diagnostic + .message + .contains("https://deno.land/x/example/mod.ts"))); + shutdown(&mut client); +} + +#[test] +fn lsp_import_map_config_file() { + let temp_dir = TempDir::new(); + let mut params: lsp::InitializeParams = + serde_json::from_value(load_fixture("initialize_params.json")).unwrap(); + + let deno_import_map_jsonc = + serde_json::to_vec_pretty(&load_fixture("deno.import_map.jsonc")).unwrap(); + fs::write( + temp_dir.path().join("deno.import_map.jsonc"), + deno_import_map_jsonc, + ) + .unwrap(); + + params.root_uri = Some(Url::from_file_path(temp_dir.path()).unwrap()); + if let Some(Value::Object(mut map)) = params.initialization_options { + map.insert("config".to_string(), json!("./deno.import_map.jsonc")); + params.initialization_options = Some(Value::Object(map)); + } + let import_map = + serde_json::to_vec_pretty(&load_fixture("import-map.json")).unwrap(); + fs::write(temp_dir.path().join("import-map.json"), import_map).unwrap(); + fs::create_dir(temp_dir.path().join("lib")).unwrap(); + fs::write( + temp_dir.path().join("lib").join("b.ts"), + r#"export const b = "b";"#, + ) + .unwrap(); + + let deno_exe = deno_exe_path(); + let mut client = LspClient::new(&deno_exe, false).unwrap(); + client + .write_request::<_, _, Value>("initialize", params) + .unwrap(); + + client.write_notification("initialized", json!({})).unwrap(); + let uri = Url::from_file_path(temp_dir.path().join("a.ts")).unwrap(); + + let diagnostics = did_open( + &mut client, + json!({ + "textDocument": { + "uri": uri, + "languageId": "typescript", + "version": 1, + "text": "import { b } from \"/~/b.ts\";\n\nconsole.log(b);\n" + } + }), + ); + + let diagnostics = diagnostics.into_iter().flat_map(|x| x.diagnostics); + assert_eq!(diagnostics.count(), 0); + + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/hover", + json!({ + "textDocument": { + "uri": uri + }, + "position": { + "line": 2, + "character": 12 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": [ + { + "language": "typescript", + "value":"(alias) const b: \"b\"\nimport b" + }, + "" + ], + "range": { + "start": { + "line": 2, + "character": 12 + }, + "end": { + "line": 2, + "character": 13 + } + } + })) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_deno_task() { + let temp_dir = TempDir::new(); + let workspace_root = temp_dir.path().canonicalize().unwrap(); + let mut params: lsp::InitializeParams = + serde_json::from_value(load_fixture("initialize_params.json")).unwrap(); + fs::write( + workspace_root.join("deno.jsonc"), + r#"{ + "tasks": { + "build": "deno test", + "some:test": "deno bundle mod.ts" + } + }"#, + ) + .unwrap(); + + params.root_uri = Some(Url::from_file_path(workspace_root).unwrap()); + + let deno_exe = deno_exe_path(); + let mut client = LspClient::new(&deno_exe, false).unwrap(); + client + .write_request::<_, _, Value>("initialize", params) + .unwrap(); + + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>("deno/task", json!(null)) + .unwrap(); + + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!([ + { + "name": "build", + "detail": "deno test" + }, + { + "name": "some:test", + "detail": "deno bundle mod.ts" + } + ])) + ); +} + +#[test] +fn lsp_import_assertions() { + let mut client = init("initialize_params_import_map.json"); + client + .write_notification( + "textDocument/didOpen", + json!({ + "textDocument": { + "uri": "file:///a/test.json", + "languageId": "json", + "version": 1, + "text": "{\"a\":1}" + } + }), + ) + .unwrap(); + handle_configuration_request( + &mut client, + json!([{ + "enable": true, + "codeLens": { + "test": true + } + }]), + ); + + let diagnostics = CollectedDiagnostics(did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/a.ts", + "languageId": "typescript", + "version": 1, + "text": "import a from \"./test.json\";\n\nconsole.log(a);\n" + } + }), + )); + + assert_eq!( + json!( + diagnostics + .with_file_and_source("file:///a/a.ts", "deno") + .diagnostics + ), + json!([ + { + "range": { + "start": { + "line": 0, + "character": 14 + }, + "end": { + "line": 0, + "character": 27 + } + }, + "severity": 1, + "code": "no-assert-type", + "source": "deno", + "message": "The module is a JSON module and not being imported with an import assertion. Consider adding `assert { type: \"json\" }` to the import statement." + } + ]) + ); + + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/codeAction", + load_fixture("code_action_params_import_assertion.json"), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("code_action_response_import_assertion.json")) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_import_map_import_completions() { + let temp_dir = TempDir::new(); + let mut params: lsp::InitializeParams = + serde_json::from_value(load_fixture("initialize_params.json")).unwrap(); + let import_map = + serde_json::to_vec_pretty(&load_fixture("import-map-completions.json")) + .unwrap(); + fs::write(temp_dir.path().join("import-map.json"), import_map).unwrap(); + fs::create_dir(temp_dir.path().join("lib")).unwrap(); + fs::write( + temp_dir.path().join("lib").join("b.ts"), + r#"export const b = "b";"#, + ) + .unwrap(); + + params.root_uri = Some(Url::from_file_path(temp_dir.path()).unwrap()); + if let Some(Value::Object(mut map)) = params.initialization_options { + map.insert("importMap".to_string(), json!("import-map.json")); + params.initialization_options = Some(Value::Object(map)); + } + + let deno_exe = deno_exe_path(); + let mut client = LspClient::new(&deno_exe, false).unwrap(); + client + .write_request::<_, _, Value>("initialize", params) + .unwrap(); + + client.write_notification("initialized", json!({})).unwrap(); + let uri = Url::from_file_path(temp_dir.path().join("a.ts")).unwrap(); + + did_open( + &mut client, + json!({ + "textDocument": { + "uri": uri, + "languageId": "typescript", + "version": 1, + "text": "import * as a from \"/~/b.ts\";\nimport * as b from \"\"" + } + }), + ); + + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/completion", + json!({ + "textDocument": { + "uri": uri + }, + "position": { + "line": 1, + "character": 20 + }, + "context": { + "triggerKind": 2, + "triggerCharacter": "\"" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "isIncomplete": false, + "items": [ + { + "label": ".", + "kind": 19, + "detail": "(local)", + "sortText": "1", + "insertText": ".", + "commitCharacters": ["\"", "'"], + }, + { + "label": "..", + "kind": 19, + "detail": "(local)", + "sortText": "1", + "insertText": "..", + "commitCharacters": ["\"", "'"], + }, + { + "label": "std", + "kind": 19, + "detail": "(import map)", + "sortText": "std", + "insertText": "std", + "commitCharacters": ["\"", "'"], + }, + { + "label": "fs", + "kind": 17, + "detail": "(import map)", + "sortText": "fs", + "insertText": "fs", + "commitCharacters": ["\"", "'"], + }, + { + "label": "/~", + "kind": 19, + "detail": "(import map)", + "sortText": "/~", + "insertText": "/~", + "commitCharacters": ["\"", "'"], + } + ] + })) + ); + + client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": uri, + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 1, + "character": 20 + }, + "end": { + "line": 1, + "character": 20 + } + }, + "text": "/~/" + } + ] + }), + ) + .unwrap(); + let (method, _) = client.read_notification::<Value>().unwrap(); + assert_eq!(method, "textDocument/publishDiagnostics"); + let (method, _) = client.read_notification::<Value>().unwrap(); + assert_eq!(method, "textDocument/publishDiagnostics"); + let (method, _) = client.read_notification::<Value>().unwrap(); + assert_eq!(method, "textDocument/publishDiagnostics"); + + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/completion", + json!({ + "textDocument": { + "uri": uri + }, + "position": { + "line": 1, + "character": 23 + }, + "context": { + "triggerKind": 2, + "triggerCharacter": "/" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "isIncomplete": false, + "items": [ + { + "label": "b.ts", + "kind": 9, + "detail": "(import map)", + "sortText": "1", + "filterText": "/~/b.ts", + "textEdit": { + "range": { + "start": { + "line": 1, + "character": 20 + }, + "end": { + "line": 1, + "character": 23 + } + }, + "newText": "/~/b.ts" + }, + "commitCharacters": ["\"", "'"], + } + ] + })) + ); + + shutdown(&mut client); +} + +#[test] +fn lsp_hover() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "console.log(Deno.args);\n" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 0, + "character": 19 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": [ + { + "language": "typescript", + "value": "const Deno.args: string[]" + }, + "Returns the script arguments to the program.\n\nGive the following command line invocation of Deno:\n\n```sh\ndeno run --allow-read https://deno.land/std/examples/cat.ts /etc/passwd\n```\n\nThen `Deno.args` will contain:\n\n```\n[ \"/etc/passwd\" ]\n```\n\nIf you are looking for a structured way to parse arguments, there is the\n[`std/flags`](https://deno.land/std/flags) module as part of the Deno\nstandard library.", + "\n\n*@category* - Runtime Environment", + ], + "range": { + "start": { + "line": 0, + "character": 17 + }, + "end": { + "line": 0, + "character": 21 + } + } + })) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_hover_asset() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "console.log(Date.now());\n" + } + }), + ); + let (_, maybe_error) = client + .write_request::<_, _, Value>( + "textDocument/definition", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 0, + "character": 14 + } + }), + ) + .unwrap(); + assert!(maybe_error.is_none()); + let (_, maybe_error) = client + .write_request::<_, _, Value>( + "deno/virtualTextDocument", + json!({ + "textDocument": { + "uri": "deno:/asset/lib.deno.shared_globals.d.ts" + } + }), + ) + .unwrap(); + assert!(maybe_error.is_none()); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "deno:/asset/lib.es2015.symbol.wellknown.d.ts" + }, + "position": { + "line": 109, + "character": 13 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": [ + { + "language": "typescript", + "value": "interface Date", + }, + "Enables basic storage and retrieval of dates and times." + ], + "range": { + "start": { + "line": 109, + "character": 10, + }, + "end": { + "line": 109, + "character": 14, + } + } + })) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_hover_disabled() { + let mut client = init("initialize_params_disabled.json"); + client + .write_notification( + "textDocument/didOpen", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "console.log(Date.now());\n" + } + }), + ) + .unwrap(); + + handle_configuration_request(&mut client, json!([{ "enable": false }])); + + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 0, + "character": 19 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(maybe_res, Some(json!(null))); + shutdown(&mut client); +} + +#[test] +fn lsp_inlay_hints() { + let mut client = init("initialize_params_hints.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": r#"function a(b: string) { + return b; + } + + a("foo"); + + enum C { + A, + } + + parseInt("123", 8); + + const d = Date.now(); + + class E { + f = Date.now(); + } + + ["a"].map((v) => v + v); + "# + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/inlayHint", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + }, + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 19, + "character": 0, + } + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + json!(maybe_res), + json!([ + { + "position": { + "line": 0, + "character": 21 + }, + "label": ": string", + "kind": 1, + "paddingLeft": true + }, + { + "position": { + "line": 4, + "character": 10 + }, + "label": "b:", + "kind": 2, + "paddingRight": true + }, + { + "position": { + "line": 7, + "character": 11 + }, + "label": "= 0", + "paddingLeft": true + }, + { + "position": { + "line": 10, + "character": 17 + }, + "label": "string:", + "kind": 2, + "paddingRight": true + }, + { + "position": { + "line": 10, + "character": 24 + }, + "label": "radix:", + "kind": 2, + "paddingRight": true + }, + { + "position": { + "line": 12, + "character": 15 + }, + "label": ": number", + "kind": 1, + "paddingLeft": true + }, + { + "position": { + "line": 15, + "character": 11 + }, + "label": ": number", + "kind": 1, + "paddingLeft": true + }, + { + "position": { + "line": 18, + "character": 18 + }, + "label": "callbackfn:", + "kind": 2, + "paddingRight": true + }, + { + "position": { + "line": 18, + "character": 20 + }, + "label": ": string", + "kind": 1, + "paddingLeft": true + }, + { + "position": { + "line": 18, + "character": 21 + }, + "label": ": string", + "kind": 1, + "paddingLeft": true + } + ]) + ); +} + +#[test] +fn lsp_inlay_hints_not_enabled() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": r#"function a(b: string) { + return b; + } + + a("foo"); + + enum C { + A, + } + + parseInt("123", 8); + + const d = Date.now(); + + class E { + f = Date.now(); + } + + ["a"].map((v) => v + v); + "# + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/inlayHint", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + }, + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 19, + "character": 0, + } + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(json!(maybe_res), json!(null)); +} + +#[test] +fn lsp_workspace_enable_paths() { + let mut params: lsp::InitializeParams = serde_json::from_value(load_fixture( + "initialize_params_workspace_enable_paths.json", + )) + .unwrap(); + // we aren't actually writing anything to the tempdir in this test, but we + // just need a legitimate file path on the host system so that logic that + // tries to convert to and from the fs paths works on all env + let temp_dir = TempDir::new(); + + let root_specifier = + ensure_directory_specifier(Url::from_file_path(temp_dir.path()).unwrap()); + + params.root_uri = Some(root_specifier.clone()); + params.workspace_folders = Some(vec![lsp::WorkspaceFolder { + uri: root_specifier.clone(), + name: "project".to_string(), + }]); + + let deno_exe = deno_exe_path(); + let mut client = LspClient::new(&deno_exe, false).unwrap(); + client + .write_request::<_, _, Value>("initialize", params) + .unwrap(); + + client.write_notification("initialized", json!({})).unwrap(); + + handle_configuration_request( + &mut client, + json!([{ + "enable": false, + "enablePaths": [ + "./worker" + ], + }]), + ); + + did_open( + &mut client, + json!({ + "textDocument": { + "uri": root_specifier.join("./file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": "console.log(Date.now());\n" + } + }), + ); + + did_open( + &mut client, + json!({ + "textDocument": { + "uri": root_specifier.join("./other/file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": "console.log(Date.now());\n" + } + }), + ); + + did_open( + &mut client, + json!({ + "textDocument": { + "uri": root_specifier.join("./worker/file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": "console.log(Date.now());\n" + } + }), + ); + + did_open( + &mut client, + json!({ + "textDocument": { + "uri": root_specifier.join("./worker/subdir/file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": "console.log(Date.now());\n" + } + }), + ); + + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": root_specifier.join("./file.ts").unwrap(), + }, + "position": { + "line": 0, + "character": 19 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(maybe_res, Some(json!(null))); + + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": root_specifier.join("./other/file.ts").unwrap(), + }, + "position": { + "line": 0, + "character": 19 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(maybe_res, Some(json!(null))); + + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": root_specifier.join("./worker/file.ts").unwrap(), + }, + "position": { + "line": 0, + "character": 19 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": [ + { + "language": "typescript", + "value": "(method) DateConstructor.now(): number", + }, + "Returns the number of milliseconds elapsed since midnight, January 1, 1970 Universal Coordinated Time (UTC)." + ], + "range": { + "start": { + "line": 0, + "character": 17, + }, + "end": { + "line": 0, + "character": 20, + } + } + })) + ); + + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": root_specifier.join("./worker/subdir/file.ts").unwrap(), + }, + "position": { + "line": 0, + "character": 19 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": [ + { + "language": "typescript", + "value": "(method) DateConstructor.now(): number", + }, + "Returns the number of milliseconds elapsed since midnight, January 1, 1970 Universal Coordinated Time (UTC)." + ], + "range": { + "start": { + "line": 0, + "character": 17, + }, + "end": { + "line": 0, + "character": 20, + } + } + })) + ); + + shutdown(&mut client); +} + +#[test] +fn lsp_hover_unstable_disabled() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "console.log(Deno.dlopen);\n" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 0, + "character": 19 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": [ + { + "language": "typescript", + "value": "any" + } + ], + "range": { + "start": { + "line": 0, + "character": 17 + }, + "end": { + "line": 0, + "character": 23 + } + } + })) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_hover_unstable_enabled() { + let mut client = init("initialize_params_unstable.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "console.log(Deno.ppid);\n" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 0, + "character": 19 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents":[ + { + "language":"typescript", + "value":"const Deno.ppid: number" + }, + "The process ID of parent process of this instance of the Deno CLI.\n\n```ts\nconsole.log(Deno.ppid);\n```", + "\n\n*@category* - Runtime Environment", + ], + "range":{ + "start":{ + "line":0, + "character":17 + }, + "end":{ + "line":0, + "character":21 + } + } + })) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_hover_change_mbc() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "const a = `ηΌεθ½―δ»ΆεΎιΎ`;\nconst b = `ππ¦π`;\nconsole.log(a, b);\n" + } + }), + ); + client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 1, + "character": 11 + }, + "end": { + "line": 1, + // the LSP uses utf16 encoded characters indexes, so + // after the deno emoiji is character index 15 + "character": 15 + } + }, + "text": "" + } + ] + }), + ) + .unwrap(); + let (method, _) = client.read_notification::<Value>().unwrap(); + assert_eq!(method, "textDocument/publishDiagnostics"); + let (method, _) = client.read_notification::<Value>().unwrap(); + assert_eq!(method, "textDocument/publishDiagnostics"); + let (method, _) = client.read_notification::<Value>().unwrap(); + assert_eq!(method, "textDocument/publishDiagnostics"); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 2, + "character": 15 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": [ + { + "language": "typescript", + "value": "const b: \"π\"", + }, + "", + ], + "range": { + "start": { + "line": 2, + "character": 15, + }, + "end": { + "line": 2, + "character": 16, + }, + } + })) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_hover_closed_document() { + let temp_dir_guard = TempDir::new(); + let temp_dir = temp_dir_guard.path(); + let a_path = temp_dir.join("a.ts"); + fs::write(a_path, r#"export const a = "a";"#).unwrap(); + let b_path = temp_dir.join("b.ts"); + fs::write(&b_path, r#"export * from "./a.ts";"#).unwrap(); + let b_specifier = Url::from_file_path(b_path).unwrap(); + let c_path = temp_dir.join("c.ts"); + fs::write(&c_path, "import { a } from \"./b.ts\";\nconsole.log(a);\n") + .unwrap(); + let c_specifier = Url::from_file_path(c_path).unwrap(); + + let mut client = init("initialize_params.json"); + client + .write_notification( + "textDocument/didOpen", + json!({ + "textDocument": { + "uri": b_specifier, + "languageId": "typescript", + "version": 1, + "text": r#"export * from "./a.ts";"# + } + }), + ) + .unwrap(); + let (id, method, _) = client.read_request::<Value>().unwrap(); + assert_eq!(method, "workspace/configuration"); + client + .write_response(id, json!([{ "enable": true }])) + .unwrap(); + + client + .write_notification( + "textDocument/didOpen", + json!({ + "textDocument": { + "uri": c_specifier, + "languageId": "typescript", + "version": 1, + "text": "import { a } from \"./b.ts\";\nconsole.log(a);\n", + } + }), + ) + .unwrap(); + let (id, method, _) = client.read_request::<Value>().unwrap(); + assert_eq!(method, "workspace/configuration"); + client + .write_response(id, json!([{ "enable": true }])) + .unwrap(); + + let (method, _) = client.read_notification::<Value>().unwrap(); + assert_eq!(method, "textDocument/publishDiagnostics"); + let (method, _) = client.read_notification::<Value>().unwrap(); + assert_eq!(method, "textDocument/publishDiagnostics"); + let (method, _) = client.read_notification::<Value>().unwrap(); + assert_eq!(method, "textDocument/publishDiagnostics"); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": c_specifier, + }, + "position": { + "line": 0, + "character": 10 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": [ + { + "language": "typescript", + "value": "(alias) const a: \"a\"\nimport a" + }, + "" + ], + "range": { + "start": { + "line": 0, + "character": 9 + }, + "end": { + "line": 0, + "character": 10 + } + } + })) + ); + client + .write_notification( + "textDocument/didClose", + json!({ + "textDocument": { + "uri": b_specifier, + } + }), + ) + .unwrap(); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": c_specifier, + }, + "position": { + "line": 0, + "character": 10 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": [ + { + "language": "typescript", + "value": "(alias) const a: \"a\"\nimport a" + }, + "" + ], + "range": { + "start": { + "line": 0, + "character": 9 + }, + "end": { + "line": 0, + "character": 10 + } + } + })) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_hover_dependency() { + let _g = http_server(); + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file_01.ts", + "languageId": "typescript", + "version": 1, + "text": "export const a = \"a\";\n", + } + }), + ); + did_open( + &mut client, + load_fixture("did_open_params_import_hover.json"), + ); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "deno/cache", + json!({ + "referrer": { + "uri": "file:///a/file.ts", + }, + "uris": [], + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + }, + "position": { + "line": 0, + "character": 28 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": { + "kind": "markdown", + "value": "**Resolved Dependency**\n\n**Code**: http​://127.0.0.1:4545/xTypeScriptTypes.js\n\n**Types**: http​://127.0.0.1:4545/xTypeScriptTypes.d.ts\n" + }, + "range": { + "start": { + "line": 0, + "character": 19 + }, + "end":{ + "line": 0, + "character": 62 + } + } + })) + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + }, + "position": { + "line": 3, + "character": 28 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": { + "kind": "markdown", + "value": "**Resolved Dependency**\n\n**Code**: http​://127.0.0.1:4545/subdir/type_reference.js\n\n**Types**: http​://127.0.0.1:4545/subdir/type_reference.d.ts\n" + }, + "range": { + "start": { + "line": 3, + "character": 19 + }, + "end":{ + "line": 3, + "character": 67 + } + } + })) + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + }, + "position": { + "line": 4, + "character": 28 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": { + "kind": "markdown", + "value": "**Resolved Dependency**\n\n**Code**: http​://127.0.0.1:4545/subdir/mod1.ts\n" + }, + "range": { + "start": { + "line": 4, + "character": 19 + }, + "end":{ + "line": 4, + "character": 57 + } + } + })) + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + }, + "position": { + "line": 5, + "character": 28 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": { + "kind": "markdown", + "value": "**Resolved Dependency**\n\n**Code**: _(a data url)_\n" + }, + "range": { + "start": { + "line": 5, + "character": 19 + }, + "end":{ + "line": 5, + "character": 132 + } + } + })) + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + }, + "position": { + "line": 6, + "character": 28 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": { + "kind": "markdown", + "value": "**Resolved Dependency**\n\n**Code**: file​:///a/file_01.ts\n" + }, + "range": { + "start": { + "line": 6, + "character": 19 + }, + "end":{ + "line": 6, + "character": 33 + } + } + })) + ); +} + +// This tests for a regression covered by denoland/deno#12753 where the lsp was +// unable to resolve dependencies when there was an invalid syntax in the module +#[test] +fn lsp_hover_deps_preserved_when_invalid_parse() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file1.ts", + "languageId": "typescript", + "version": 1, + "text": "export type Foo = { bar(): string };\n" + } + }), + ); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file2.ts", + "languageId": "typescript", + "version": 1, + "text": "import { Foo } from './file1.ts'; declare const f: Foo; f\n" + } + }), + ); + let (maybe_res, maybe_error) = client + .write_request::<_, _, Value>( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file2.ts" + }, + "position": { + "line": 0, + "character": 56 + } + }), + ) + .unwrap(); + assert!(maybe_error.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": [ + { + "language": "typescript", + "value": "const f: Foo", + }, + "" + ], + "range": { + "start": { + "line": 0, + "character": 56, + }, + "end": { + "line": 0, + "character": 57, + } + } + })) + ); + client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": "file:///a/file2.ts", + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 0, + "character": 57 + }, + "end": { + "line": 0, + "character": 58 + } + }, + "text": "." + } + ] + }), + ) + .unwrap(); + let (maybe_res, maybe_error) = client + .write_request::<_, _, Value>( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file2.ts" + }, + "position": { + "line": 0, + "character": 56 + } + }), + ) + .unwrap(); + assert!(maybe_error.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": [ + { + "language": "typescript", + "value": "const f: Foo", + }, + "" + ], + "range": { + "start": { + "line": 0, + "character": 56, + }, + "end": { + "line": 0, + "character": 57, + } + } + })) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_hover_typescript_types() { + let _g = http_server(); + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "import * as a from \"http://127.0.0.1:4545/xTypeScriptTypes.js\";\n\nconsole.log(a.foo);\n", + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "deno/cache", + json!({ + "referrer": { + "uri": "file:///a/file.ts", + }, + "uris": [ + { + "uri": "http://127.0.0.1:4545/xTypeScriptTypes.js", + } + ], + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 0, + "character": 24 + } + }), + ) + .unwrap(); + assert!(maybe_res.is_some()); + assert!(maybe_err.is_none()); + assert_eq!( + json!(maybe_res.unwrap()), + json!({ + "contents": { + "kind": "markdown", + "value": "**Resolved Dependency**\n\n**Code**: http​://127.0.0.1:4545/xTypeScriptTypes.js\n\n**Types**: http​://127.0.0.1:4545/xTypeScriptTypes.d.ts\n" + }, + "range": { + "start": { + "line": 0, + "character": 19 + }, + "end": { + "line": 0, + "character": 62 + } + } + }) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_hover_jsdoc_symbol_link() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/b.ts", + "languageId": "typescript", + "version": 1, + "text": "export function hello() {}\n" + } + }), + ); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "import { hello } from \"./b.ts\";\n\nhello();\n\nconst b = \"b\";\n\n/** JSDoc {@link hello} and {@linkcode b} */\nfunction a() {}\n" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 7, + "character": 10 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": [ + { + "language": "typescript", + "value": "function a(): void" + }, + "JSDoc [hello](file:///a/file.ts#L1,10) and [`b`](file:///a/file.ts#L5,7)" + ], + "range": { + "start": { + "line": 7, + "character": 9 + }, + "end": { + "line": 7, + "character": 10 + } + } + })) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_goto_type_definition() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "interface A {\n a: string;\n}\n\nexport class B implements A {\n a = \"a\";\n log() {\n console.log(this.a);\n }\n}\n\nconst b = new B();\nb;\n", + } + }), + ); + let (maybe_res, maybe_error) = client + .write_request::<_, _, Value>( + "textDocument/typeDefinition", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 12, + "character": 1 + } + }), + ) + .unwrap(); + assert!(maybe_error.is_none()); + assert_eq!( + maybe_res, + Some(json!([ + { + "targetUri": "file:///a/file.ts", + "targetRange": { + "start": { + "line": 4, + "character": 0 + }, + "end": { + "line": 9, + "character": 1 + } + }, + "targetSelectionRange": { + "start": { + "line": 4, + "character": 13 + }, + "end": { + "line": 4, + "character": 14 + } + } + } + ])) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_call_hierarchy() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "function foo() {\n return false;\n}\n\nclass Bar {\n baz() {\n return foo();\n }\n}\n\nfunction main() {\n const bar = new Bar();\n bar.baz();\n}\n\nmain();" + } + }), + ); + let (maybe_res, maybe_error) = client + .write_request( + "textDocument/prepareCallHierarchy", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 5, + "character": 3 + } + }), + ) + .unwrap(); + assert!(maybe_error.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("prepare_call_hierarchy_response.json")) + ); + let (maybe_res, maybe_error) = client + .write_request( + "callHierarchy/incomingCalls", + load_fixture("incoming_calls_params.json"), + ) + .unwrap(); + assert!(maybe_error.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("incoming_calls_response.json")) + ); + let (maybe_res, maybe_error) = client + .write_request( + "callHierarchy/outgoingCalls", + load_fixture("outgoing_calls_params.json"), + ) + .unwrap(); + assert!(maybe_error.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("outgoing_calls_response.json")) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_large_doc_changes() { + let mut client = init("initialize_params.json"); + did_open(&mut client, load_fixture("did_open_params_large.json")); + client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 444, + "character": 11 + }, + "end": { + "line": 444, + "character": 14 + } + }, + "text": "+++" + } + ] + }), + ) + .unwrap(); + client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 445, + "character": 4 + }, + "end": { + "line": 445, + "character": 4 + } + }, + "text": "// " + } + ] + }), + ) + .unwrap(); + client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 477, + "character": 4 + }, + "end": { + "line": 477, + "character": 9 + } + }, + "text": "error" + } + ] + }), + ) + .unwrap(); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 421, + "character": 30 + } + }), + ) + .unwrap(); + assert!(maybe_res.is_some()); + assert!(maybe_err.is_none()); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 444, + "character": 6 + } + }), + ) + .unwrap(); + assert!(maybe_res.is_some()); + assert!(maybe_err.is_none()); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 461, + "character": 34 + } + }), + ) + .unwrap(); + assert!(maybe_res.is_some()); + assert!(maybe_err.is_none()); + shutdown(&mut client); + + assert!(client.duration().as_millis() <= 15000); +} + +#[test] +fn lsp_document_symbol() { + let mut client = init("initialize_params.json"); + did_open(&mut client, load_fixture("did_open_params_doc_symbol.json")); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/documentSymbol", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("document_symbol_response.json")) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_folding_range() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "// #region 1\n/*\n * Some comment\n */\nclass Foo {\n bar(a, b) {\n if (a === b) {\n return true;\n }\n return false;\n }\n}\n// #endregion" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/foldingRange", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!([ + { + "startLine": 0, + "endLine": 12, + "kind": "region" + }, + { + "startLine": 1, + "endLine": 3, + "kind": "comment" + }, + { + "startLine": 4, + "endLine": 10 + }, + { + "startLine": 5, + "endLine": 9 + }, + { + "startLine": 6, + "endLine": 7 + } + ])) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_rename() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + // this should not rename in comments and strings + "text": "let variable = 'a'; // variable\nconsole.log(variable);\n\"variable\";\n" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/rename", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 0, + "character": 4 + }, + "newName": "variable_modified" + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(maybe_res, Some(load_fixture("rename_response.json"))); + shutdown(&mut client); +} + +#[test] +fn lsp_selection_range() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "class Foo {\n bar(a, b) {\n if (a === b) {\n return true;\n }\n return false;\n }\n}" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/selectionRange", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "positions": [ + { + "line": 2, + "character": 8 + } + ] + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("selection_range_response.json")) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_semantic_tokens() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + load_fixture("did_open_params_semantic_tokens.json"), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/semanticTokens/full", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "data": [ + 0, 5, 6, 1, 1, 0, 9, 6, 8, 9, 0, 8, 6, 8, 9, 2, 15, 3, 10, 5, 0, 4, 1, + 6, 1, 0, 12, 7, 2, 16, 1, 8, 1, 7, 41, 0, 4, 1, 6, 0, 0, 2, 5, 11, 16, + 1, 9, 1, 7, 40, 3, 10, 4, 2, 1, 1, 11, 1, 9, 9, 1, 2, 3, 11, 1, 3, 6, 3, + 0, 1, 0, 15, 4, 2, 0, 1, 30, 1, 6, 9, 1, 2, 3, 11,1, 1, 9, 9, 9, 3, 0, + 16, 3, 0, 0, 1, 17, 12, 11, 3, 0, 24, 3, 0, 0, 0, 4, 9, 9, 2 + ] + })) + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/semanticTokens/range", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 6, + "character": 0 + } + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "data": [ + 0, 5, 6, 1, 1, 0, 9, 6, 8, 9, 0, 8, 6, 8, 9, 2, 15, 3, 10, 5, 0, 4, 1, + 6, 1, 0, 12, 7, 2, 16, 1, 8, 1, 7, 41, 0, 4, 1, 6, 0, 0, 2, 5, 11, 16, + 1, 9, 1, 7, 40 + ] + })) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_code_lens() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "class A {\n a = \"a\";\n\n b() {\n console.log(this.a);\n }\n\n c() {\n this.a = \"c\";\n }\n}\n\nconst a = new A();\na.b();\n" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/codeLens", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(maybe_res, Some(load_fixture("code_lens_response.json"))); + let (maybe_res, maybe_err) = client + .write_request( + "codeLens/resolve", + json!({ + "range": { + "start": { + "line": 0, + "character": 6 + }, + "end": { + "line": 0, + "character": 7 + } + }, + "data": { + "specifier": "file:///a/file.ts", + "source": "references" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("code_lens_resolve_response.json")) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_code_lens_impl() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "interface A {\n b(): void;\n}\n\nclass B implements A {\n b() {\n console.log(\"b\");\n }\n}\n\ninterface C {\n c: string;\n}\n" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/codeLens", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("code_lens_response_impl.json")) + ); + let (maybe_res, maybe_err) = client + .write_request( + "codeLens/resolve", + json!({ + "range": { + "start": { + "line": 0, + "character": 10 + }, + "end": { + "line": 0, + "character": 11 + } + }, + "data": { + "specifier": "file:///a/file.ts", + "source": "implementations" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("code_lens_resolve_response_impl.json")) + ); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "codeLens/resolve", + json!({ + "range": { + "start": { + "line": 10, + "character": 10 + }, + "end": { + "line": 10, + "character": 11 + } + }, + "data": { + "specifier": "file:///a/file.ts", + "source": "implementations" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "range": { + "start": { + "line": 10, + "character": 10 + }, + "end": { + "line": 10, + "character": 11 + } + }, + "command": { + "title": "0 implementations", + "command": "" + } + })) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_code_lens_test() { + let mut client = init("initialize_params_code_lens_test.json"); + did_open( + &mut client, + load_fixture("did_open_params_test_code_lens.json"), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/codeLens", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("code_lens_response_test.json")) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_code_lens_test_disabled() { + let mut client = init("initialize_params_code_lens_test_disabled.json"); + client + .write_notification( + "textDocument/didOpen", + load_fixture("did_open_params_test_code_lens.json"), + ) + .unwrap(); + + let (id, method, _) = client.read_request::<Value>().unwrap(); + assert_eq!(method, "workspace/configuration"); + client + .write_response( + id, + json!([{ + "enable": true, + "codeLens": { + "test": false + } + }]), + ) + .unwrap(); + + let (method, _) = client.read_notification::<Value>().unwrap(); + assert_eq!(method, "textDocument/publishDiagnostics"); + let (method, _) = client.read_notification::<Value>().unwrap(); + assert_eq!(method, "textDocument/publishDiagnostics"); + let (method, _) = client.read_notification::<Value>().unwrap(); + assert_eq!(method, "textDocument/publishDiagnostics"); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/codeLens", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(maybe_res, Some(json!([]))); + shutdown(&mut client); +} + +#[test] +fn lsp_code_lens_non_doc_nav_tree() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "console.log(Date.now());\n" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/references", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 0, + "character": 3 + }, + "context": { + "includeDeclaration": true + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "deno/virtualTextDocument", + json!({ + "textDocument": { + "uri": "deno:/asset/lib.deno.shared_globals.d.ts" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Vec<lsp::CodeLens>>( + "textDocument/codeLens", + json!({ + "textDocument": { + "uri": "deno:/asset/lib.deno.shared_globals.d.ts" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + let res = maybe_res.unwrap(); + assert!(res.len() > 50); + let (maybe_res, maybe_err) = client + .write_request::<_, _, lsp::CodeLens>( + "codeLens/resolve", + json!({ + "range": { + "start": { + "line": 416, + "character": 12 + }, + "end": { + "line": 416, + "character": 19 + } + }, + "data": { + "specifier": "asset:///lib.deno.shared_globals.d.ts", + "source": "references" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + shutdown(&mut client); +} + +#[test] +fn lsp_nav_tree_updates() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "interface A {\n b(): void;\n}\n\nclass B implements A {\n b() {\n console.log(\"b\");\n }\n}\n\ninterface C {\n c: string;\n}\n" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/codeLens", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("code_lens_response_impl.json")) + ); + client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 10, + "character": 0 + }, + "end": { + "line": 13, + "character": 0 + } + }, + "text": "" + } + ] + }), + ) + .unwrap(); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/codeLens", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("code_lens_response_changed.json")) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_signature_help() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "/**\n * Adds two numbers.\n * @param a This is a first number.\n * @param b This is a second number.\n */\nfunction add(a: number, b: number) {\n return a + b;\n}\n\nadd(" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/signatureHelp", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "character": 4, + "line": 9 + }, + "context": { + "triggerKind": 2, + "triggerCharacter": "(", + "isRetrigger": false + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "signatures": [ + { + "label": "add(a: number, b: number): number", + "documentation": { + "kind": "markdown", + "value": "Adds two numbers." + }, + "parameters": [ + { + "label": "a: number", + "documentation": { + "kind": "markdown", + "value": "This is a first number." + } + }, + { + "label": "b: number", + "documentation": { + "kind": "markdown", + "value": "This is a second number." + } + } + ] + } + ], + "activeSignature": 0, + "activeParameter": 0 + })) + ); + client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 9, + "character": 4 + }, + "end": { + "line": 9, + "character": 4 + } + }, + "text": "123, " + } + ] + }), + ) + .unwrap(); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/signatureHelp", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "character": 8, + "line": 9 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "signatures": [ + { + "label": "add(a: number, b: number): number", + "documentation": { + "kind": "markdown", + "value": "Adds two numbers." + }, + "parameters": [ + { + "label": "a: number", + "documentation": { + "kind": "markdown", + "value": "This is a first number." + } + }, + { + "label": "b: number", + "documentation": { + "kind": "markdown", + "value": "This is a second number." + } + } + ] + } + ], + "activeSignature": 0, + "activeParameter": 1 + })) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_code_actions() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "export function a(): void {\n await Promise.resolve(\"a\");\n}\n\nexport function b(): void {\n await Promise.resolve(\"b\");\n}\n" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/codeAction", + load_fixture("code_action_params.json"), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(maybe_res, Some(load_fixture("code_action_response.json"))); + let (maybe_res, maybe_err) = client + .write_request( + "codeAction/resolve", + load_fixture("code_action_resolve_params.json"), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("code_action_resolve_response.json")) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_code_actions_deno_cache() { + let mut session = TestSession::from_file("initialize_params.json"); + let diagnostics = session.did_open(json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "import * as a from \"https://deno.land/x/a/mod.ts\";\n\nconsole.log(a);\n" + } + })); + assert_eq!( + diagnostics.with_source("deno"), + load_fixture_as("diagnostics_deno_deps.json") + ); + + let (maybe_res, maybe_err) = session + .client + .write_request( + "textDocument/codeAction", + load_fixture("code_action_params_cache.json"), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("code_action_response_cache.json")) + ); + session.shutdown_and_exit(); +} + +#[test] +fn lsp_code_actions_deno_cache_npm() { + let mut session = TestSession::from_file("initialize_params.json"); + let diagnostics = session.did_open(json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "import chalk from \"npm:chalk\";\n\nconsole.log(chalk.green);\n" + } + })); + assert_eq!( + diagnostics.with_source("deno"), + load_fixture_as("code_actions/cache_npm/diagnostics.json") + ); + + let (maybe_res, maybe_err) = session + .client + .write_request( + "textDocument/codeAction", + load_fixture("code_actions/cache_npm/cache_action.json"), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("code_actions/cache_npm/cache_response.json")) + ); + session.shutdown_and_exit(); +} + +#[test] +fn lsp_code_actions_imports() { + let mut session = TestSession::from_file("initialize_params.json"); + session.did_open(json!({ + "textDocument": { + "uri": "file:///a/file00.ts", + "languageId": "typescript", + "version": 1, + "text": r#"export interface MallardDuckConfigOptions extends DuckConfigOptions { + kind: "mallard"; +} + +export class MallardDuckConfig extends DuckConfig { + constructor(options: MallardDuckConfigOptions) { + super(options); + } +} +"# + } + })); + session.did_open(json!({ + "textDocument": { + "uri": "file:///a/file01.ts", + "languageId": "typescript", + "version": 1, + "text": r#"import { DuckConfigOptions } from "./file02.ts"; + +export class DuckConfig { + readonly kind; + constructor(options: DuckConfigOptions) { + this.kind = options.kind; + } +} +"# + } + })); + session.did_open(json!({ + "textDocument": { + "uri": "file:///a/file02.ts", + "languageId": "typescript", + "version": 1, + "text": r#"export interface DuckConfigOptions { + kind: string; + quacks: boolean; +} +"# + } + })); + + let (maybe_res, maybe_err) = session + .client + .write_request( + "textDocument/codeAction", + load_fixture("code_action_params_imports.json"), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("code_action_response_imports.json")) + ); + let (maybe_res, maybe_err) = session + .client + .write_request( + "codeAction/resolve", + load_fixture("code_action_resolve_params_imports.json"), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("code_action_resolve_response_imports.json")) + ); + + session.shutdown_and_exit(); +} + +#[test] +fn lsp_code_actions_refactor() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "var x: { a?: number; b?: string } = {};\n" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/codeAction", + load_fixture("code_action_params_refactor.json"), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("code_action_response_refactor.json")) + ); + let (maybe_res, maybe_err) = client + .write_request( + "codeAction/resolve", + load_fixture("code_action_resolve_params_refactor.json"), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("code_action_resolve_response_refactor.json")) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_code_actions_refactor_no_disabled_support() { + let mut client = init("initialize_params_ca_no_disabled.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "interface A {\n a: string;\n}\n\ninterface B {\n b: string;\n}\n\nclass AB implements A, B {\n a = \"a\";\n b = \"b\";\n}\n\nnew AB().a;\n" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/codeAction", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 14, + "character": 0 + } + }, + "context": { + "diagnostics": [], + "only": [ + "refactor" + ] + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("code_action_response_no_disabled.json")) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_code_actions_deadlock() { + let mut client = init("initialize_params.json"); + client + .write_notification( + "textDocument/didOpen", + load_fixture("did_open_params_large.json"), + ) + .unwrap(); + let (id, method, _) = client.read_request::<Value>().unwrap(); + assert_eq!(method, "workspace/configuration"); + client + .write_response(id, json!([{ "enable": true }])) + .unwrap(); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/semanticTokens/full", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + read_diagnostics(&mut client); + client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 444, + "character": 11 + }, + "end": { + "line": 444, + "character": 14 + } + }, + "text": "+++" + } + ] + }), + ) + .unwrap(); + client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 445, + "character": 4 + }, + "end": { + "line": 445, + "character": 4 + } + }, + "text": "// " + } + ] + }), + ) + .unwrap(); + client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 477, + "character": 4 + }, + "end": { + "line": 477, + "character": 9 + } + }, + "text": "error" + } + ] + }), + ) + .unwrap(); + // diagnostics only trigger after changes have elapsed in a separate thread, + // so we need to delay the next messages a little bit to attempt to create a + // potential for a deadlock with the codeAction + std::thread::sleep(std::time::Duration::from_millis(50)); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + }, + "position": { + "line": 609, + "character": 33, + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/codeAction", + load_fixture("code_action_params_deadlock.json"), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + + read_diagnostics(&mut client); + + shutdown(&mut client); +} + +#[test] +fn lsp_completions() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "Deno." + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/completion", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 0, + "character": 5 + }, + "context": { + "triggerKind": 2, + "triggerCharacter": "." + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + if let Some(lsp::CompletionResponse::List(list)) = maybe_res { + assert!(!list.is_incomplete); + assert!(list.items.len() > 90); + } else { + panic!("unexpected response"); + } + let (maybe_res, maybe_err) = client + .write_request( + "completionItem/resolve", + load_fixture("completion_resolve_params.json"), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("completion_resolve_response.json")) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_completions_private_fields() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": r#"class Foo { #myProperty = "value"; constructor() { this.# } }"# + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/completion", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 0, + "character": 57 + }, + "context": { + "triggerKind": 1 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + if let Some(lsp::CompletionResponse::List(list)) = maybe_res { + assert_eq!(list.items.len(), 1); + let item = &list.items[0]; + assert_eq!(item.label, "#myProperty"); + assert!(!list.is_incomplete); + } else { + panic!("unexpected response"); + } + shutdown(&mut client); +} + +#[test] +fn lsp_completions_optional() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "interface A {\n b?: string;\n}\n\nconst o: A = {};\n\nfunction c(s: string) {}\n\nc(o.)" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/completion", + load_fixture("completion_request_params_optional.json"), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "isIncomplete": false, + "items": [ + { + "label": "b?", + "kind": 5, + "sortText": "11", + "filterText": "b", + "insertText": "b", + "commitCharacters": [".", ",", ";", "("], + "data": { + "tsc": { + "specifier": "file:///a/file.ts", + "position": 79, + "name": "b", + "useCodeSnippet": false + } + } + } + ] + })) + ); + let (maybe_res, maybe_err) = client + .write_request( + "completionItem/resolve", + load_fixture("completion_resolve_params_optional.json"), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "label": "b?", + "kind": 5, + "detail": "(property) A.b?: string | undefined", + "documentation": { + "kind": "markdown", + "value": "" + }, + "sortText": "1", + "filterText": "b", + "insertText": "b" + })) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_completions_auto_import() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/b.ts", + "languageId": "typescript", + "version": 1, + "text": "export const foo = \"foo\";\n", + } + }), + ); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "export {};\n\n", + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/completion", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 2, + "character": 0, + }, + "context": { + "triggerKind": 1, + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + if let Some(lsp::CompletionResponse::List(list)) = maybe_res { + assert!(!list.is_incomplete); + if !list.items.iter().any(|item| item.label == "foo") { + panic!("completions items missing 'foo' symbol"); + } + } else { + panic!("unexpected completion response"); + } + let (maybe_res, maybe_err) = client + .write_request( + "completionItem/resolve", + json!({ + "label": "foo", + "kind": 6, + "sortText": "οΏΏ16", + "commitCharacters": [ + ".", + ",", + ";", + "(" + ], + "data": { + "tsc": { + "specifier": "file:///a/file.ts", + "position": 12, + "name": "foo", + "source": "./b", + "data": { + "exportName": "foo", + "moduleSpecifier": "./b", + "fileName": "file:///a/b.ts" + }, + "useCodeSnippet": false + } + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "label": "foo", + "kind": 6, + "detail": "const foo: \"foo\"", + "documentation": { + "kind": "markdown", + "value": "" + }, + "sortText": "οΏΏ16", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "import { foo } from \"./b.ts\";\n\n" + } + ] + })) + ); +} + +#[test] +fn lsp_completions_snippet() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/a.tsx", + "languageId": "typescriptreact", + "version": 1, + "text": "function A({ type }: { type: string }) {\n return type;\n}\n\nfunction B() {\n return <A t\n}", + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/completion", + json!({ + "textDocument": { + "uri": "file:///a/a.tsx" + }, + "position": { + "line": 5, + "character": 13, + }, + "context": { + "triggerKind": 1, + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + if let Some(lsp::CompletionResponse::List(list)) = maybe_res { + assert!(!list.is_incomplete); + assert_eq!( + json!(list), + json!({ + "isIncomplete": false, + "items": [ + { + "label": "type", + "kind": 5, + "sortText": "11", + "filterText": "type=\"$1\"", + "insertText": "type=\"$1\"", + "insertTextFormat": 2, + "commitCharacters": [ + ".", + ",", + ";", + "(" + ], + "data": { + "tsc": { + "specifier": "file:///a/a.tsx", + "position": 87, + "name": "type", + "useCodeSnippet": false + } + } + } + ] + }) + ); + } else { + panic!("unexpected completion response"); + } + let (maybe_res, maybe_err) = client + .write_request( + "completionItem/resolve", + json!({ + "label": "type", + "kind": 5, + "sortText": "11", + "filterText": "type=\"$1\"", + "insertText": "type=\"$1\"", + "insertTextFormat": 2, + "commitCharacters": [ + ".", + ",", + ";", + "(" + ], + "data": { + "tsc": { + "specifier": "file:///a/a.tsx", + "position": 87, + "name": "type", + "useCodeSnippet": false + } + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "label": "type", + "kind": 5, + "detail": "(property) type: string", + "documentation": { + "kind": "markdown", + "value": "" + }, + "sortText": "11", + "filterText": "type=\"$1\"", + "insertText": "type=\"$1\"", + "insertTextFormat": 2 + })) + ); +} + +#[test] +fn lsp_completions_no_snippet() { + let mut client = init("initialize_params_no_snippet.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/a.tsx", + "languageId": "typescriptreact", + "version": 1, + "text": "function A({ type }: { type: string }) {\n return type;\n}\n\nfunction B() {\n return <A t\n}", + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/completion", + json!({ + "textDocument": { + "uri": "file:///a/a.tsx" + }, + "position": { + "line": 5, + "character": 13, + }, + "context": { + "triggerKind": 1, + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + if let Some(lsp::CompletionResponse::List(list)) = maybe_res { + assert!(!list.is_incomplete); + assert_eq!( + json!(list), + json!({ + "isIncomplete": false, + "items": [ + { + "label": "type", + "kind": 5, + "sortText": "11", + "commitCharacters": [ + ".", + ",", + ";", + "(" + ], + "data": { + "tsc": { + "specifier": "file:///a/a.tsx", + "position": 87, + "name": "type", + "useCodeSnippet": false + } + } + } + ] + }) + ); + } else { + panic!("unexpected completion response"); + } +} + +#[test] +fn lsp_completions_npm() { + let _g = http_server(); + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "import cjsDefault from 'npm:@denotest/cjs-default-export';import chalk from 'npm:chalk';\n\n", + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "deno/cache", + json!({ + "referrer": { + "uri": "file:///a/file.ts", + }, + "uris": [ + { + "uri": "npm:@denotest/cjs-default-export", + }, + { + "uri": "npm:chalk", + } + ] + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + + // check importing a cjs default import + client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 0 + } + }, + "text": "cjsDefault." + } + ] + }), + ) + .unwrap(); + read_diagnostics(&mut client); + + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/completion", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 2, + "character": 11 + }, + "context": { + "triggerKind": 2, + "triggerCharacter": "." + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + if let Some(lsp::CompletionResponse::List(list)) = maybe_res { + assert!(!list.is_incomplete); + assert_eq!(list.items.len(), 3); + assert!(list.items.iter().any(|i| i.label == "default")); + assert!(list.items.iter().any(|i| i.label == "MyClass")); + } else { + panic!("unexpected response"); + } + let (maybe_res, maybe_err) = client + .write_request( + "completionItem/resolve", + load_fixture("completions/npm/resolve_params.json"), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("completions/npm/resolve_response.json")) + ); + + // now check chalk, which is esm + client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "version": 3 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 11 + } + }, + "text": "chalk." + } + ] + }), + ) + .unwrap(); + read_diagnostics(&mut client); + + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/completion", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 2, + "character": 6 + }, + "context": { + "triggerKind": 2, + "triggerCharacter": "." + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + if let Some(lsp::CompletionResponse::List(list)) = maybe_res { + assert!(!list.is_incomplete); + assert!(list.items.iter().any(|i| i.label == "green")); + assert!(list.items.iter().any(|i| i.label == "red")); + } else { + panic!("unexpected response"); + } + + shutdown(&mut client); +} + +#[test] +fn lsp_npm_specifier_unopened_file() { + let _g = http_server(); + let mut client = init("initialize_params.json"); + + // create other.ts, which re-exports an npm specifier + client.deno_dir().write( + "other.ts", + "export { default as chalk } from 'npm:chalk@5';", + ); + + // cache the other.ts file to the DENO_DIR + let deno = deno_cmd_with_deno_dir(client.deno_dir()) + .current_dir(client.deno_dir().path()) + .arg("cache") + .arg("--quiet") + .arg("other.ts") + .envs(env_vars_for_npm_tests()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + let output = deno.wait_with_output().unwrap(); + assert!(output.status.success()); + assert_eq!(output.status.code(), Some(0)); + + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.is_empty()); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!(stderr.is_empty()); + + // open main.ts, which imports other.ts (unopened) + let main_url = + ModuleSpecifier::from_file_path(client.deno_dir().path().join("main.ts")) + .unwrap(); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": main_url, + "languageId": "typescript", + "version": 1, + "text": "import { chalk } from './other.ts';\n\n", + } + }), + ); + + client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": main_url, + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 0 + } + }, + "text": "chalk." + } + ] + }), + ) + .unwrap(); + read_diagnostics(&mut client); + + // now ensure completions work + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/completion", + json!({ + "textDocument": { + "uri": main_url + }, + "position": { + "line": 2, + "character": 6 + }, + "context": { + "triggerKind": 2, + "triggerCharacter": "." + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + if let Some(lsp::CompletionResponse::List(list)) = maybe_res { + assert!(!list.is_incomplete); + assert_eq!(list.items.len(), 63); + assert!(list.items.iter().any(|i| i.label == "ansi256")); + } else { + panic!("unexpected response"); + } +} + +#[test] +fn lsp_completions_registry() { + let _g = http_server(); + let mut client = init("initialize_params_registry.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "import * as a from \"http://localhost:4545/x/a@\"" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/completion", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 0, + "character": 46 + }, + "context": { + "triggerKind": 2, + "triggerCharacter": "@" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + if let Some(lsp::CompletionResponse::List(list)) = maybe_res { + assert!(!list.is_incomplete); + assert_eq!(list.items.len(), 3); + } else { + panic!("unexpected response"); + } + let (maybe_res, maybe_err) = client + .write_request( + "completionItem/resolve", + load_fixture("completion_resolve_params_registry.json"), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("completion_resolve_response_registry.json")) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_completions_registry_empty() { + let _g = http_server(); + let mut client = init("initialize_params_registry.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "import * as a from \"\"" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/completion", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 0, + "character": 20 + }, + "context": { + "triggerKind": 2, + "triggerCharacter": "\"" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("completion_request_response_empty.json")) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_auto_discover_registry() { + let _g = http_server(); + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "import * as a from \"http://localhost:4545/x/a@\"" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/completion", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 0, + "character": 46 + }, + "context": { + "triggerKind": 2, + "triggerCharacter": "@" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + let (method, maybe_res) = client.read_notification().unwrap(); + assert_eq!(method, "deno/registryState"); + assert_eq!( + maybe_res, + Some(json!({ + "origin": "http://localhost:4545", + "suggestions": true, + })) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_cache_location() { + let _g = http_server(); + let temp_dir = TempDir::new(); + let mut params: lsp::InitializeParams = + serde_json::from_value(load_fixture("initialize_params_registry.json")) + .unwrap(); + + params.root_uri = Some(Url::from_file_path(temp_dir.path()).unwrap()); + if let Some(Value::Object(mut map)) = params.initialization_options { + map.insert("cache".to_string(), json!(".cache")); + params.initialization_options = Some(Value::Object(map)); + } + + let deno_exe = deno_exe_path(); + let mut client = LspClient::new(&deno_exe, false).unwrap(); + client + .write_request::<_, _, Value>("initialize", params) + .unwrap(); + client.write_notification("initialized", json!({})).unwrap(); + let mut session = TestSession::from_client(client); + + session.did_open(json!({ + "textDocument": { + "uri": "file:///a/file_01.ts", + "languageId": "typescript", + "version": 1, + "text": "export const a = \"a\";\n", + } + })); + let diagnostics = + session.did_open(load_fixture("did_open_params_import_hover.json")); + assert_eq!(diagnostics.viewed().len(), 7); + let (maybe_res, maybe_err) = session + .client + .write_request::<_, _, Value>( + "deno/cache", + json!({ + "referrer": { + "uri": "file:///a/file.ts", + }, + "uris": [], + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + let (maybe_res, maybe_err) = session + .client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + }, + "position": { + "line": 0, + "character": 28 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": { + "kind": "markdown", + "value": "**Resolved Dependency**\n\n**Code**: http​://127.0.0.1:4545/xTypeScriptTypes.js\n\n**Types**: http​://127.0.0.1:4545/xTypeScriptTypes.d.ts\n" + }, + "range": { + "start": { + "line": 0, + "character": 19 + }, + "end":{ + "line": 0, + "character": 62 + } + } + })) + ); + let (maybe_res, maybe_err) = session + .client + .write_request::<_, _, Value>( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + }, + "position": { + "line": 7, + "character": 28 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": { + "kind": "markdown", + "value": "**Resolved Dependency**\n\n**Code**: http​://localhost:4545/x/a/mod.ts\n\n\n---\n\n**a**\n\nmod.ts" + }, + "range": { + "start": { + "line": 7, + "character": 19 + }, + "end": { + "line": 7, + "character": 53 + } + } + })) + ); + let cache_path = temp_dir.path().join(".cache"); + assert!(cache_path.is_dir()); + assert!(cache_path.join("gen").is_dir()); + session.shutdown_and_exit(); +} + +/// Sets the TLS root certificate on startup, which allows the LSP to connect to +/// the custom signed test server and be able to retrieve the registry config +/// and cache files. +#[test] +fn lsp_tls_cert() { + let _g = http_server(); + let mut params: lsp::InitializeParams = + serde_json::from_value(load_fixture("initialize_params_tls_cert.json")) + .unwrap(); + + params.root_uri = Some(Url::from_file_path(testdata_path()).unwrap()); + + let deno_exe = deno_exe_path(); + let mut client = LspClient::new(&deno_exe, false).unwrap(); + client + .write_request::<_, _, Value>("initialize", params) + .unwrap(); + client.write_notification("initialized", json!({})).unwrap(); + let mut session = TestSession::from_client(client); + + session.did_open(json!({ + "textDocument": { + "uri": "file:///a/file_01.ts", + "languageId": "typescript", + "version": 1, + "text": "export const a = \"a\";\n", + } + })); + let diagnostics = + session.did_open(load_fixture("did_open_params_tls_cert.json")); + let diagnostics = diagnostics.viewed(); + assert_eq!(diagnostics.len(), 7); + let (maybe_res, maybe_err) = session + .client + .write_request::<_, _, Value>( + "deno/cache", + json!({ + "referrer": { + "uri": "file:///a/file.ts", + }, + "uris": [], + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + let (maybe_res, maybe_err) = session + .client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + }, + "position": { + "line": 0, + "character": 28 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": { + "kind": "markdown", + "value": "**Resolved Dependency**\n\n**Code**: https​://localhost:5545/xTypeScriptTypes.js\n" + }, + "range": { + "start": { + "line": 0, + "character": 19 + }, + "end":{ + "line": 0, + "character": 63 + } + } + })) + ); + let (maybe_res, maybe_err) = session + .client + .write_request::<_, _, Value>( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + }, + "position": { + "line": 7, + "character": 28 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": { + "kind": "markdown", + "value": "**Resolved Dependency**\n\n**Code**: http​://localhost:4545/x/a/mod.ts\n\n\n---\n\n**a**\n\nmod.ts" + }, + "range": { + "start": { + "line": 7, + "character": 19 + }, + "end": { + "line": 7, + "character": 53 + } + } + })) + ); + session.shutdown_and_exit(); +} + +#[test] +fn lsp_diagnostics_warn_redirect() { + let _g = http_server(); + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "import * as a from \"http://127.0.0.1:4545/x_deno_warning.js\";\n\nconsole.log(a)\n", + }, + }), + ); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "deno/cache", + json!({ + "referrer": { + "uri": "file:///a/file.ts", + }, + "uris": [ + { + "uri": "http://127.0.0.1:4545/x_deno_warning.js", + } + ], + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + let diagnostics = read_diagnostics(&mut client); + assert_eq!( + diagnostics.with_source("deno"), + lsp::PublishDiagnosticsParams { + uri: Url::parse("file:///a/file.ts").unwrap(), + diagnostics: vec![ + lsp::Diagnostic { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 19 + }, + end: lsp::Position { + line: 0, + character: 60 + } + }, + severity: Some(lsp::DiagnosticSeverity::WARNING), + code: Some(lsp::NumberOrString::String("deno-warn".to_string())), + source: Some("deno".to_string()), + message: "foobar".to_string(), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 19 + }, + end: lsp::Position { + line: 0, + character: 60 + } + }, + severity: Some(lsp::DiagnosticSeverity::INFORMATION), + code: Some(lsp::NumberOrString::String("redirect".to_string())), + source: Some("deno".to_string()), + message: "The import of \"http://127.0.0.1:4545/x_deno_warning.js\" was redirected to \"http://127.0.0.1:4545/lsp/x_deno_warning_redirect.js\".".to_string(), + data: Some(json!({"specifier": "http://127.0.0.1:4545/x_deno_warning.js", "redirect": "http://127.0.0.1:4545/lsp/x_deno_warning_redirect.js"})), + ..Default::default() + } + ], + version: Some(1), + } + ); + shutdown(&mut client); +} + +#[test] +fn lsp_redirect_quick_fix() { + let _g = http_server(); + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "import * as a from \"http://127.0.0.1:4545/x_deno_warning.js\";\n\nconsole.log(a)\n", + }, + }), + ); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "deno/cache", + json!({ + "referrer": { + "uri": "file:///a/file.ts", + }, + "uris": [ + { + "uri": "http://127.0.0.1:4545/x_deno_warning.js", + } + ], + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + let diagnostics = read_diagnostics(&mut client) + .with_source("deno") + .diagnostics; + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/codeAction", + json!(json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "range": { + "start": { + "line": 0, + "character": 19 + }, + "end": { + "line": 0, + "character": 60 + } + }, + "context": { + "diagnostics": diagnostics, + "only": [ + "quickfix" + ] + } + })), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("code_action_redirect_response.json")) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_diagnostics_deprecated() { + let mut client = init("initialize_params.json"); + let diagnostics = did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "/** @deprecated */\nexport const a = \"a\";\n\na;\n", + }, + }), + ); + assert_eq!( + json!(diagnostics), + json!([ + { + "uri": "file:///a/file.ts", + "diagnostics": [], + "version": 1 + }, + { + "uri": "file:///a/file.ts", + "diagnostics": [], + "version": 1 + }, + { + "uri": "file:///a/file.ts", + "diagnostics": [ + { + "range": { + "start": { + "line": 3, + "character": 0 + }, + "end": { + "line": 3, + "character": 1 + } + }, + "severity": 4, + "code": 6385, + "source": "deno-ts", + "message": "'a' is deprecated.", + "relatedInformation": [], + "tags": [ + 2 + ] + } + ], + "version": 1 + } + ]) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_diagnostics_deno_types() { + let mut client = init("initialize_params.json"); + client + .write_notification( + "textDocument/didOpen", + load_fixture("did_open_params_deno_types.json"), + ) + .unwrap(); + let (id, method, _) = client.read_request::<Value>().unwrap(); + assert_eq!(method, "workspace/configuration"); + client + .write_response(id, json!([{ "enable": true }])) + .unwrap(); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/documentSymbol", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + } + }), + ) + .unwrap(); + assert!(maybe_res.is_some()); + assert!(maybe_err.is_none()); + let diagnostics = read_diagnostics(&mut client); + assert_eq!(diagnostics.viewed().len(), 5); + shutdown(&mut client); +} + +#[test] +fn lsp_diagnostics_refresh_dependents() { + let mut session = TestSession::from_file("initialize_params.json"); + session.did_open(json!({ + "textDocument": { + "uri": "file:///a/file_00.ts", + "languageId": "typescript", + "version": 1, + "text": "export const a = \"a\";\n", + }, + })); + session.did_open(json!({ + "textDocument": { + "uri": "file:///a/file_01.ts", + "languageId": "typescript", + "version": 1, + "text": "export * from \"./file_00.ts\";\n", + }, + })); + let diagnostics = session.did_open(json!({ + "textDocument": { + "uri": "file:///a/file_02.ts", + "languageId": "typescript", + "version": 1, + "text": "import { a, b } from \"./file_01.ts\";\n\nconsole.log(a, b);\n" + } + })); + assert_eq!( + json!(diagnostics.with_file_and_source("file:///a/file_02.ts", "deno-ts")), + json!({ + "uri": "file:///a/file_02.ts", + "diagnostics": [ + { + "range": { + "start": { + "line": 0, + "character": 12 + }, + "end": { + "line": 0, + "character": 13 + } + }, + "severity": 1, + "code": 2305, + "source": "deno-ts", + "message": "Module '\"./file_01.ts\"' has no exported member 'b'." + } + ], + "version": 1 + }) + ); + + // fix the code causing the diagnostic + session + .client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": "file:///a/file_00.ts", + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "text": "export const b = \"b\";\n" + } + ] + }), + ) + .unwrap(); + let diagnostics = session.read_diagnostics(); + assert_eq!(diagnostics.viewed().len(), 0); // no diagnostics now + + session.shutdown_and_exit(); + assert_eq!(session.client.queue_len(), 0); +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PerformanceAverage { + pub name: String, + pub count: u32, + pub average_duration: u32, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct PerformanceAverages { + averages: Vec<PerformanceAverage>, +} + +#[test] +fn lsp_performance() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "console.log(Deno.args);\n" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 0, + "character": 19 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + let (maybe_res, maybe_err) = client + .write_request::<_, _, PerformanceAverages>("deno/performance", json!(null)) + .unwrap(); + assert!(maybe_err.is_none()); + if let Some(res) = maybe_res { + assert_eq!(res.averages.len(), 13); + } else { + panic!("unexpected result"); + } + shutdown(&mut client); +} + +#[test] +fn lsp_format_no_changes() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "console;\n" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/formatting", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "options": { + "tabSize": 2, + "insertSpaces": true + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(maybe_res, Some(json!(null))); + client.assert_no_notification("window/showMessage"); + shutdown(&mut client); +} + +#[test] +fn lsp_format_error() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "console test test\n" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/formatting", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "options": { + "tabSize": 2, + "insertSpaces": true + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(maybe_res, Some(json!(null))); + shutdown(&mut client); +} + +#[test] +fn lsp_format_mbc() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "const bar = 'ππΊπΈπ'\nconsole.log('hello deno')\n" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/formatting", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "options": { + "tabSize": 2, + "insertSpaces": true + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!(load_fixture("formatting_mbc_response.json"))) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_format_exclude_with_config() { + let temp_dir = TempDir::new(); + let mut params: lsp::InitializeParams = + serde_json::from_value(load_fixture("initialize_params.json")).unwrap(); + let deno_fmt_jsonc = + serde_json::to_vec_pretty(&load_fixture("deno.fmt.exclude.jsonc")).unwrap(); + fs::write(temp_dir.path().join("deno.fmt.jsonc"), deno_fmt_jsonc).unwrap(); + + params.root_uri = Some(Url::from_file_path(temp_dir.path()).unwrap()); + if let Some(Value::Object(mut map)) = params.initialization_options { + map.insert("config".to_string(), json!("./deno.fmt.jsonc")); + params.initialization_options = Some(Value::Object(map)); + } + + let deno_exe = deno_exe_path(); + let mut client = LspClient::new(&deno_exe, false).unwrap(); + client + .write_request::<_, _, Value>("initialize", params) + .unwrap(); + + let file_uri = + ModuleSpecifier::from_file_path(temp_dir.path().join("ignored.ts")) + .unwrap() + .to_string(); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": file_uri, + "languageId": "typescript", + "version": 1, + "text": "function myFunc(){}" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/formatting", + json!({ + "textDocument": { + "uri": file_uri + }, + "options": { + "tabSize": 2, + "insertSpaces": true + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(maybe_res, Some(json!(null))); + shutdown(&mut client); +} + +#[test] +fn lsp_format_exclude_default_config() { + let temp_dir = TempDir::new(); + let workspace_root = temp_dir.path().canonicalize().unwrap(); + let mut params: lsp::InitializeParams = + serde_json::from_value(load_fixture("initialize_params.json")).unwrap(); + let deno_jsonc = + serde_json::to_vec_pretty(&load_fixture("deno.fmt.exclude.jsonc")).unwrap(); + fs::write(workspace_root.join("deno.jsonc"), deno_jsonc).unwrap(); + + params.root_uri = Some(Url::from_file_path(workspace_root.clone()).unwrap()); + + let deno_exe = deno_exe_path(); + let mut client = LspClient::new(&deno_exe, false).unwrap(); + client + .write_request::<_, _, Value>("initialize", params) + .unwrap(); + + let file_uri = + ModuleSpecifier::from_file_path(workspace_root.join("ignored.ts")) + .unwrap() + .to_string(); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": file_uri, + "languageId": "typescript", + "version": 1, + "text": "function myFunc(){}" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/formatting", + json!({ + "textDocument": { + "uri": file_uri + }, + "options": { + "tabSize": 2, + "insertSpaces": true + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(maybe_res, Some(json!(null))); + shutdown(&mut client); +} + +#[test] +fn lsp_format_json() { + let mut client = init("initialize_params.json"); + client + .write_notification( + "textDocument/didOpen", + json!({ + "textDocument": { + // Also test out using a non-json file extension here. + // What should matter is the language identifier. + "uri": "file:///a/file.lock", + "languageId": "json", + "version": 1, + "text": "{\"key\":\"value\"}" + } + }), + ) + .unwrap(); + + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/formatting", + json!({ + "textDocument": { + "uri": "file:///a/file.lock" + }, + "options": { + "tabSize": 2, + "insertSpaces": true + } + }), + ) + .unwrap(); + + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!([ + { + "range": { + "start": { + "line": 0, + "character": 1 + }, + "end": { + "line": 0, + "character": 1 + } + }, + "newText": " " + }, + { + "range": { + "start": { "line": 0, "character": 7 }, + "end": { "line": 0, "character": 7 } + }, + "newText": " " + }, + { + "range": { + "start": { "line": 0, "character": 14 }, + "end": { "line": 0, "character": 15 } + }, + "newText": " }\n" + } + ])) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_json_no_diagnostics() { + let mut client = init("initialize_params.json"); + client + .write_notification( + "textDocument/didOpen", + json!({ + "textDocument": { + "uri": "file:///a/file.json", + "languageId": "json", + "version": 1, + "text": "{\"key\":\"value\"}" + } + }), + ) + .unwrap(); + + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/semanticTokens/full", + json!({ + "textDocument": { + "uri": "file:///a/file.json" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(maybe_res, Some(json!(null))); + + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.json" + }, + "position": { + "line": 0, + "character": 3 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(maybe_res, Some(json!(null))); + + shutdown(&mut client); +} + +#[test] +fn lsp_format_markdown() { + let mut client = init("initialize_params.json"); + client + .write_notification( + "textDocument/didOpen", + json!({ + "textDocument": { + "uri": "file:///a/file.md", + "languageId": "markdown", + "version": 1, + "text": "# Hello World" + } + }), + ) + .unwrap(); + + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/formatting", + json!({ + "textDocument": { + "uri": "file:///a/file.md" + }, + "options": { + "tabSize": 2, + "insertSpaces": true + } + }), + ) + .unwrap(); + + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!([ + { + "range": { + "start": { "line": 0, "character": 1 }, + "end": { "line": 0, "character": 3 } + }, + "newText": "" + }, + { + "range": { + "start": { "line": 0, "character": 15 }, + "end": { "line": 0, "character": 15 } + }, + "newText": "\n" + } + ])) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_format_with_config() { + let temp_dir = TempDir::new(); + let mut params: lsp::InitializeParams = + serde_json::from_value(load_fixture("initialize_params.json")).unwrap(); + let deno_fmt_jsonc = + serde_json::to_vec_pretty(&load_fixture("deno.fmt.jsonc")).unwrap(); + fs::write(temp_dir.path().join("deno.fmt.jsonc"), deno_fmt_jsonc).unwrap(); + + params.root_uri = Some(Url::from_file_path(temp_dir.path()).unwrap()); + if let Some(Value::Object(mut map)) = params.initialization_options { + map.insert("config".to_string(), json!("./deno.fmt.jsonc")); + params.initialization_options = Some(Value::Object(map)); + } + + let deno_exe = deno_exe_path(); + let mut client = LspClient::new(&deno_exe, false).unwrap(); + client + .write_request::<_, _, Value>("initialize", params) + .unwrap(); + + client + .write_notification( + "textDocument/didOpen", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "export async function someVeryLongFunctionName() {\nconst response = fetch(\"http://localhost:4545/some/non/existent/path.json\");\nconsole.log(response.text());\nconsole.log(\"finished!\")\n}" + } + }), + ) + .unwrap(); + + // The options below should be ignored in favor of configuration from config file. + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/formatting", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "options": { + "tabSize": 2, + "insertSpaces": true + } + }), + ) + .unwrap(); + + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!([{ + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "newText": "\t" + }, + { + "range": { + "start": { + "line": 1, + "character": 23 + }, + "end": { + "line": 1, + "character": 24 + } + }, + "newText": "\n\t\t'" + }, + { + "range": { + "start": { + "line": 1, + "character": 73 + }, + "end": { + "line": 1, + "character": 74 + } + }, + "newText": "',\n\t" + }, + { + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 0 + } + }, + "newText": "\t" + }, + { + "range": { + "start": { + "line": 3, + "character": 0 + }, + "end": { + "line": 3, + "character": 0 + } + }, + "newText": "\t" + }, + { + "range": { + "start": { + "line": 3, + "character": 12 + }, + "end": { + "line": 3, + "character": 13 + } + }, + "newText": "'" + }, + { + "range": { + "start": { + "line": 3, + "character": 22 + }, + "end": { + "line": 3, + "character": 24 + } + }, + "newText": "');" + }, + { + "range": { + "start": { + "line": 4, + "character": 1 + }, + "end": { + "line": 4, + "character": 1 + } + }, + "newText": "\n" + }] + )) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_markdown_no_diagnostics() { + let mut client = init("initialize_params.json"); + client + .write_notification( + "textDocument/didOpen", + json!({ + "textDocument": { + "uri": "file:///a/file.md", + "languageId": "markdown", + "version": 1, + "text": "# Hello World" + } + }), + ) + .unwrap(); + + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/semanticTokens/full", + json!({ + "textDocument": { + "uri": "file:///a/file.md" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(maybe_res, Some(json!(null))); + + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.md" + }, + "position": { + "line": 0, + "character": 3 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(maybe_res, Some(json!(null))); + + shutdown(&mut client); +} + +#[test] +fn lsp_configuration_did_change() { + let _g = http_server(); + let mut client = init("initialize_params_did_config_change.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "import * as a from \"http://localhost:4545/x/a@\"" + } + }), + ); + client + .write_notification( + "workspace/didChangeConfiguration", + json!({ + "settings": {} + }), + ) + .unwrap(); + let (id, method, _) = client.read_request::<Value>().unwrap(); + assert_eq!(method, "workspace/configuration"); + client + .write_response( + id, + json!([{ + "enable": true, + "codeLens": { + "implementations": true, + "references": true + }, + "importMap": null, + "lint": true, + "suggest": { + "autoImports": true, + "completeFunctionCalls": false, + "names": true, + "paths": true, + "imports": { + "hosts": { + "http://localhost:4545/": true + } + } + }, + "unstable": false + }]), + ) + .unwrap(); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/completion", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 0, + "character": 46 + }, + "context": { + "triggerKind": 2, + "triggerCharacter": "@" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + if let Some(lsp::CompletionResponse::List(list)) = maybe_res { + assert!(!list.is_incomplete); + assert_eq!(list.items.len(), 3); + } else { + panic!("unexpected response"); + } + let (maybe_res, maybe_err) = client + .write_request( + "completionItem/resolve", + load_fixture("completion_resolve_params_registry.json"), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("completion_resolve_response_registry.json")) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_workspace_symbol() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "export class A {\n fieldA: string;\n fieldB: string;\n}\n", + } + }), + ); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file_01.ts", + "languageId": "typescript", + "version": 1, + "text": "export class B {\n fieldC: string;\n fieldD: string;\n}\n", + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "workspace/symbol", + json!({ + "query": "field" + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!([ + { + "name": "fieldA", + "kind": 8, + "location": { + "uri": "file:///a/file.ts", + "range": { + "start": { + "line": 1, + "character": 2 + }, + "end": { + "line": 1, + "character": 17 + } + } + }, + "containerName": "A" + }, + { + "name": "fieldB", + "kind": 8, + "location": { + "uri": "file:///a/file.ts", + "range": { + "start": { + "line": 2, + "character": 2 + }, + "end": { + "line": 2, + "character": 17 + } + } + }, + "containerName": "A" + }, + { + "name": "fieldC", + "kind": 8, + "location": { + "uri": "file:///a/file_01.ts", + "range": { + "start": { + "line": 1, + "character": 2 + }, + "end": { + "line": 1, + "character": 17 + } + } + }, + "containerName": "B" + }, + { + "name": "fieldD", + "kind": 8, + "location": { + "uri": "file:///a/file_01.ts", + "range": { + "start": { + "line": 2, + "character": 2 + }, + "end": { + "line": 2, + "character": 17 + } + } + }, + "containerName": "B" + } + ])) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_code_actions_ignore_lint() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "let message = 'Hello, Deno!';\nconsole.log(message);\n" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/codeAction", + load_fixture("code_action_ignore_lint_params.json"), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("code_action_ignore_lint_response.json")) + ); + shutdown(&mut client); +} + +/// This test exercises updating an existing deno-lint-ignore-file comment. +#[test] +fn lsp_code_actions_update_ignore_lint() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": +"#!/usr/bin/env -S deno run +// deno-lint-ignore-file camelcase +let snake_case = 'Hello, Deno!'; +console.log(snake_case); +", + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/codeAction", + load_fixture("code_action_update_ignore_lint_params.json"), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(load_fixture("code_action_update_ignore_lint_response.json")) + ); + shutdown(&mut client); +} + +#[test] +fn lsp_lint_with_config() { + let temp_dir = TempDir::new(); + let mut params: lsp::InitializeParams = + serde_json::from_value(load_fixture("initialize_params.json")).unwrap(); + let deno_lint_jsonc = + serde_json::to_vec_pretty(&load_fixture("deno.lint.jsonc")).unwrap(); + fs::write(temp_dir.path().join("deno.lint.jsonc"), deno_lint_jsonc).unwrap(); + + params.root_uri = Some(Url::from_file_path(temp_dir.path()).unwrap()); + if let Some(Value::Object(mut map)) = params.initialization_options { + map.insert("config".to_string(), json!("./deno.lint.jsonc")); + params.initialization_options = Some(Value::Object(map)); + } + + let deno_exe = deno_exe_path(); + let mut client = LspClient::new(&deno_exe, false).unwrap(); + client + .write_request::<_, _, Value>("initialize", params) + .unwrap(); + let mut session = TestSession::from_client(client); + + let diagnostics = session.did_open(load_fixture("did_open_lint.json")); + let diagnostics = diagnostics.viewed(); + assert_eq!(diagnostics.len(), 1); + assert_eq!( + diagnostics[0].code, + Some(lsp::NumberOrString::String("ban-untagged-todo".to_string())) + ); + session.shutdown_and_exit(); +} + +#[test] +fn lsp_lint_exclude_with_config() { + let temp_dir = TempDir::new(); + let mut params: lsp::InitializeParams = + serde_json::from_value(load_fixture("initialize_params.json")).unwrap(); + let deno_lint_jsonc = + serde_json::to_vec_pretty(&load_fixture("deno.lint.exclude.jsonc")) + .unwrap(); + fs::write(temp_dir.path().join("deno.lint.jsonc"), deno_lint_jsonc).unwrap(); + + params.root_uri = Some(Url::from_file_path(temp_dir.path()).unwrap()); + if let Some(Value::Object(mut map)) = params.initialization_options { + map.insert("config".to_string(), json!("./deno.lint.jsonc")); + params.initialization_options = Some(Value::Object(map)); + } + + let deno_exe = deno_exe_path(); + let mut client = LspClient::new(&deno_exe, false).unwrap(); + client + .write_request::<_, _, Value>("initialize", params) + .unwrap(); + + let diagnostics = did_open( + &mut client, + json!({ + "textDocument": { + "uri": ModuleSpecifier::from_file_path(temp_dir.path().join("ignored.ts")).unwrap().to_string(), + "languageId": "typescript", + "version": 1, + "text": "// TODO: fixme\nexport async function non_camel_case() {\nconsole.log(\"finished!\")\n}" + } + }), + ); + let diagnostics = diagnostics + .into_iter() + .flat_map(|x| x.diagnostics) + .collect::<Vec<_>>(); + assert_eq!(diagnostics, Vec::new()); + shutdown(&mut client); +} + +#[test] +fn lsp_jsx_import_source_pragma() { + let _g = http_server(); + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.tsx", + "languageId": "typescriptreact", + "version": 1, + "text": +"/** @jsxImportSource http://localhost:4545/jsx */ + +function A() { + return \"hello\"; +} + +export function B() { + return <A></A>; +} +", + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "deno/cache", + json!({ + "referrer": { + "uri": "file:///a/file.tsx", + }, + "uris": [ + { + "uri": "http://127.0.0.1:4545/jsx/jsx-runtime", + } + ], + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.tsx" + }, + "position": { + "line": 0, + "character": 25 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": { + "kind": "markdown", + "value": "**Resolved Dependency**\n\n**Code**: http​://localhost:4545/jsx/jsx-runtime\n", + }, + "range": { + "start": { + "line": 0, + "character": 21 + }, + "end": { + "line": 0, + "character": 46 + } + } + })) + ); + shutdown(&mut client); +} + +#[derive(Debug, Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct TestData { + id: String, + label: String, + steps: Option<Vec<TestData>>, + range: Option<lsp::Range>, +} + +#[derive(Debug, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +enum TestModuleNotificationKind { + Insert, + Replace, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct TestModuleNotificationParams { + text_document: lsp::TextDocumentIdentifier, + kind: TestModuleNotificationKind, + label: String, + tests: Vec<TestData>, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct EnqueuedTestModule { + text_document: lsp::TextDocumentIdentifier, + ids: Vec<String>, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct TestRunResponseParams { + enqueued: Vec<EnqueuedTestModule>, +} + +#[test] +fn lsp_testing_api() { + let mut params: lsp::InitializeParams = + serde_json::from_value(load_fixture("initialize_params.json")).unwrap(); + let temp_dir = TempDir::new(); + + let root_specifier = + ensure_directory_specifier(Url::from_file_path(temp_dir.path()).unwrap()); + + let module_path = temp_dir.path().join("./test.ts"); + let specifier = ModuleSpecifier::from_file_path(&module_path).unwrap(); + let contents = r#" +Deno.test({ + name: "test a", + fn() { + console.log("test a"); + } +}); +"#; + fs::write(&module_path, contents).unwrap(); + fs::write(temp_dir.path().join("./deno.jsonc"), r#"{}"#).unwrap(); + + params.root_uri = Some(root_specifier); + + let deno_exe = deno_exe_path(); + let mut client = LspClient::new(&deno_exe, false).unwrap(); + client + .write_request::<_, _, Value>("initialize", params) + .unwrap(); + + client.write_notification("initialized", json!({})).unwrap(); + + client + .write_notification( + "textDocument/didOpen", + json!({ + "textDocument": { + "uri": specifier, + "languageId": "typescript", + "version": 1, + "text": contents, + } + }), + ) + .unwrap(); + + handle_configuration_request( + &mut client, + json!([{ + "enable": true, + "codeLens": { + "test": true + } + }]), + ); + + for _ in 0..4 { + let result = client.read_notification::<Value>(); + assert!(result.is_ok()); + let (method, notification) = result.unwrap(); + if method.as_str() == "deno/testModule" { + let params: TestModuleNotificationParams = + serde_json::from_value(notification.unwrap()).unwrap(); + assert_eq!(params.text_document.uri, specifier); + assert_eq!(params.kind, TestModuleNotificationKind::Replace); + assert_eq!(params.label, "test.ts"); + assert_eq!(params.tests.len(), 1); + let test = ¶ms.tests[0]; + assert_eq!(test.label, "test a"); + assert!(test.steps.is_none()); + assert_eq!( + test.range, + Some(lsp::Range { + start: lsp::Position { + line: 1, + character: 5, + }, + end: lsp::Position { + line: 1, + character: 9, + } + }) + ); + } + } + + let (maybe_res, maybe_err) = client + .write_request::<_, _, TestRunResponseParams>( + "deno/testRun", + json!({ + "id": 1, + "kind": "run", + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + let res = maybe_res.unwrap(); + assert_eq!(res.enqueued.len(), 1); + assert_eq!(res.enqueued[0].text_document.uri, specifier); + assert_eq!(res.enqueued[0].ids.len(), 1); + let id = res.enqueued[0].ids[0].clone(); + + let res = client.read_notification::<Value>(); + assert!(res.is_ok()); + let (method, notification) = res.unwrap(); + assert_eq!(method, "deno/testRunProgress"); + assert_eq!( + notification, + Some(json!({ + "id": 1, + "message": { + "type": "started", + "test": { + "textDocument": { + "uri": specifier, + }, + "id": id, + }, + } + })) + ); + + let res = client.read_notification::<Value>(); + assert!(res.is_ok()); + let (method, notification) = res.unwrap(); + assert_eq!(method, "deno/testRunProgress"); + let notification_value = notification + .as_ref() + .unwrap() + .as_object() + .unwrap() + .get("message") + .unwrap() + .as_object() + .unwrap() + .get("value") + .unwrap() + .as_str() + .unwrap(); + // deno test's output capturing flushes with a zero-width space in order to + // synchronize the output pipes. Occassionally this zero width space + // might end up in the output so strip it from the output comparison here. + assert_eq!(notification_value.replace('\u{200B}', ""), "test a\r\n"); + assert_eq!( + notification, + Some(json!({ + "id": 1, + "message": { + "type": "output", + "value": notification_value, + "test": { + "textDocument": { + "uri": specifier, + }, + "id": id, + }, + } + })) + ); + + let res = client.read_notification::<Value>(); + assert!(res.is_ok()); + let (method, notification) = res.unwrap(); + assert_eq!(method, "deno/testRunProgress"); + let notification = notification.unwrap(); + let obj = notification.as_object().unwrap(); + assert_eq!(obj.get("id"), Some(&json!(1))); + let message = obj.get("message").unwrap().as_object().unwrap(); + match message.get("type").and_then(|v| v.as_str()) { + Some("passed") => { + assert_eq!( + message.get("test"), + Some(&json!({ + "textDocument": { + "uri": specifier + }, + "id": id, + })) + ); + assert!(message.contains_key("duration")); + + let res = client.read_notification::<Value>(); + assert!(res.is_ok()); + let (method, notification) = res.unwrap(); + assert_eq!(method, "deno/testRunProgress"); + assert_eq!( + notification, + Some(json!({ + "id": 1, + "message": { + "type": "end", + } + })) + ); + } + // sometimes on windows, the messages come out of order, but it actually is + // working, so if we do get the end before the passed, we will simply let + // the test pass + Some("end") => (), + _ => panic!("unexpected message {}", json!(notification)), + } + + shutdown(&mut client); +} |