// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use deno_ast::ModuleSpecifier; use deno_core::serde::Deserialize; 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::fs; use std::str::FromStr; use test_util::assert_starts_with; use test_util::assertions::assert_json_subset; use test_util::deno_cmd_with_deno_dir; use test_util::env_vars_for_npm_tests; use test_util::lsp::range_of; use test_util::lsp::source_file; use test_util::lsp::LspClient; use test_util::testdata_path; use test_util::TestContextBuilder; use tower_lsp::lsp_types as lsp; #[test] fn lsp_startup_shutdown() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.shutdown(); } #[test] fn lsp_init_tsconfig() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write( "lib.tsconfig.json", r#"{ "compilerOptions": { "lib": ["deno.ns", "deno.unstable", "dom"] } }"#, ); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_config("lib.tsconfig.json"); }); let diagnostics = client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "location.pathname;\n" } })); assert_eq!(diagnostics.all().len(), 0); client.shutdown(); } #[test] fn lsp_tsconfig_types() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write( "types.tsconfig.json", r#"{ "compilerOptions": { "types": ["./a.d.ts"] }, "lint": { "rules": { "tags": [] } } }"#, ); let a_dts = "// deno-lint-ignore-file no-var\ndeclare var a: string;"; temp_dir.write("a.d.ts", a_dts); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder .set_config("types.tsconfig.json") // avoid finding the declaration file via the document preload .set_preload_limit(0); }); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("test.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "console.log(a);\n" } })); assert_eq!(json!(diagnostics.all()), json!([])); client.shutdown(); } #[test] fn lsp_tsconfig_types_config_sub_dir() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); let sub_dir = temp_dir.path().join("sub_dir"); sub_dir.create_dir_all(); sub_dir.join("types.tsconfig.json").write( r#"{ "compilerOptions": { "types": ["./a.d.ts"] }, "lint": { "rules": { "tags": [] } } }"#, ); let a_dts = "// deno-lint-ignore-file no-var\ndeclare var a: string;"; sub_dir.join("a.d.ts").write(a_dts); temp_dir.write("deno.json", "{}"); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder .set_config("sub_dir/types.tsconfig.json") // avoid finding the declaration file via the document preload .set_preload_limit(0); }); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("test.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "console.log(a);\n" } })); assert_eq!(json!(diagnostics.all()), json!([])); client.shutdown(); } #[test] fn lsp_triple_slash_types() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); let a_dts = "// deno-lint-ignore-file no-var\ndeclare var a: string;"; temp_dir.write("a.d.ts", a_dts); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("test.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "/// \n\nconsole.log(a);\n" } })); assert_eq!(diagnostics.all().len(), 0); client.shutdown(); } #[test] fn unadded_dependency_message_with_import_map() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.write( "import_map.json", json!({ "imports": { } }) .to_string(), ); temp_dir.write( "deno.json", json!({ "importMap": "import_map.json".to_string(), }) .to_string(), ); temp_dir.write( "file.ts", r#" import * as x from "@std/fs"; "#, ); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], temp_dir.url().join("file.ts").unwrap()], }), ); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": temp_dir.read_to_string("file.ts"), } })); // expected lsp_messages don't include the file path let mut expected_lsp_messages = Vec::from(["`x` is never used\nIf this is intentional, prefix it with an underscore like `_x`", "'x' is declared but its value is never read.", "Relative import path \"@std/fs\" not prefixed with / or ./ or ../ and not in import map from \" Hint: Use [deno add @std/fs] to add the dependency."]); expected_lsp_messages.sort(); let all_diagnostics = diagnostics.all(); let mut correct_lsp_messages = all_diagnostics .iter() .map(|d| d.message.as_str()) .collect::>(); correct_lsp_messages.sort(); let part1 = correct_lsp_messages[1].split("file").collect::>()[0]; let part2 = correct_lsp_messages[1].split('\n').collect::>()[1]; let file_path_removed_from_message = format!("{} {}", part1, part2); correct_lsp_messages[1] = file_path_removed_from_message.as_str(); assert_eq!(correct_lsp_messages, expected_lsp_messages); client.shutdown(); } #[test] fn unadded_dependency_message() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.json", json!({ "imports": { } }) .to_string(), ); temp_dir.write( "file.ts", r#" import * as x from "@std/fs"; "#, ); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], temp_dir.url().join("file.ts").unwrap()], }), ); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": temp_dir.read_to_string("file.ts"), } })); // expected lsp_messages don't include the file path let mut expected_lsp_messages = Vec::from(["`x` is never used\nIf this is intentional, prefix it with an underscore like `_x`", "'x' is declared but its value is never read.", "Relative import path \"@std/fs\" not prefixed with / or ./ or ../ and not in import map from \" Hint: Use [deno add @std/fs] to add the dependency."]); expected_lsp_messages.sort(); let all_diagnostics = diagnostics.all(); let mut correct_lsp_messages = all_diagnostics .iter() .map(|d| d.message.as_str()) .collect::>(); correct_lsp_messages.sort(); let part1 = correct_lsp_messages[1].split("file").collect::>()[0]; let part2 = correct_lsp_messages[1].split('\n').collect::>()[1]; let file_path_removed_from_message = format!("{} {}", part1, part2); correct_lsp_messages[1] = file_path_removed_from_message.as_str(); assert_eq!(correct_lsp_messages, expected_lsp_messages); client.shutdown(); } #[test] fn lsp_import_map() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); let import_map = r#"{ "imports": { "/~/": "./lib/" } }"#; temp_dir.write("import-map.json", import_map); temp_dir.create_dir_all("lib"); temp_dir.write("lib/b.ts", r#"export const b = "b";"#); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_import_map("import-map.json"); }); let uri = temp_dir.url().join("a.ts").unwrap(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": uri, "languageId": "typescript", "version": 1, "text": "import { b } from \"/~/b.ts\";\n\nconsole.log(b);\n" } })); assert_eq!(json!(diagnostics.all()), json!([])); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": uri }, "position": { "line": 2, "character": 12 } }), ); assert_eq!( res, json!({ "contents": [ { "language": "typescript", "value":"(alias) const b: \"b\"\nimport b" }, "" ], "range": { "start": { "line": 2, "character": 12 }, "end": { "line": 2, "character": 13 } } }) ); client.shutdown(); } #[test] fn lsp_import_map_remote() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.write( "file.ts", r#" import { printHello } from "print_hello"; printHello(); "#, ); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_import_map( "http://localhost:4545/import_maps/import_map_remote.json", ); }); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], temp_dir.url().join("file.ts").unwrap()], }), ); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": temp_dir.read_to_string("file.ts"), } })); assert_eq!(diagnostics.all(), vec![]); client.shutdown(); } #[test] fn lsp_import_map_data_url() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_import_map("data:application/json;utf8,{\"imports\": { \"example\": \"https://deno.land/x/example/mod.ts\" }}"); }); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import example from \"example\";\n" } })); // This indicates that the import map is applied correctly. assert!(diagnostics.all().iter().any(|diagnostic| diagnostic.code == Some(lsp::NumberOrString::String("no-cache".to_string())) && diagnostic .message .contains("https://deno.land/x/example/mod.ts"))); client.shutdown(); } #[test] fn lsp_import_map_config_file() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.import_map.jsonc", r#"{ "importMap": "import-map.json" }"#, ); temp_dir.write( "import-map.json", r#"{ "imports": { "/~/": "./lib/" } }"#, ); temp_dir.create_dir_all("lib"); temp_dir.write("lib/b.ts", r#"export const b = "b";"#); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_config("./deno.import_map.jsonc"); }); let uri = temp_dir.url().join("a.ts").unwrap(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": uri, "languageId": "typescript", "version": 1, "text": "import { b } from \"/~/b.ts\";\n\nconsole.log(b);\n" } })); assert_eq!(diagnostics.all().len(), 0); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": uri }, "position": { "line": 2, "character": 12 } }), ); assert_eq!( res, json!({ "contents": [ { "language": "typescript", "value":"(alias) const b: \"b\"\nimport b" }, "" ], "range": { "start": { "line": 2, "character": 12 }, "end": { "line": 2, "character": 13 } } }) ); client.shutdown(); } #[test] fn lsp_import_map_embedded_in_config_file() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.embedded_import_map.jsonc", r#"{ // some comment "imports": { "/~/": "./lib/" } }"#, ); temp_dir.create_dir_all("lib"); temp_dir.write("lib/b.ts", r#"export const b = "b";"#); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_config("./deno.embedded_import_map.jsonc"); }); let uri = temp_dir.url().join("a.ts").unwrap(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": uri, "languageId": "typescript", "version": 1, "text": "import { b } from \"/~/b.ts\";\n\nconsole.log(b);\n" } })); assert_eq!(diagnostics.all().len(), 0); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": uri }, "position": { "line": 2, "character": 12 } }), ); assert_eq!( res, json!({ "contents": [ { "language": "typescript", "value":"(alias) const b: \"b\"\nimport b" }, "" ], "range": { "start": { "line": 2, "character": 12 }, "end": { "line": 2, "character": 13 } } }) ); client.shutdown(); } #[test] fn lsp_import_map_embedded_in_config_file_after_initialize() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write("deno.embedded_import_map.jsonc", "{}"); temp_dir.create_dir_all("lib"); temp_dir.write("lib/b.ts", r#"export const b = "b";"#); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_config("./deno.embedded_import_map.jsonc"); }); let uri = temp_dir.url().join("a.ts").unwrap(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": uri, "languageId": "typescript", "version": 1, "text": "import { b } from \"/~/b.ts\";\n\nconsole.log(b);\n" } })); assert_eq!(diagnostics.all().len(), 1); // update the import map temp_dir.write( "deno.embedded_import_map.jsonc", r#"{ "imports": { "/~/": "./lib/" } }"#, ); client.did_change_watched_files(json!({ "changes": [{ "uri": temp_dir.url().join("deno.embedded_import_map.jsonc").unwrap(), "type": 2 }] })); assert_eq!(json!(client.read_diagnostics().all()), json!([])); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": uri }, "position": { "line": 2, "character": 12 } }), ); assert_eq!( res, json!({ "contents": [ { "language": "typescript", "value":"(alias) const b: \"b\"\nimport b" }, "" ], "range": { "start": { "line": 2, "character": 12 }, "end": { "line": 2, "character": 13 } } }) ); client.shutdown(); } #[test] fn lsp_import_map_config_file_auto_discovered() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("lib"); temp_dir.write("lib/b.ts", r#"export const b = "b";"#); let mut client = context.new_lsp_command().capture_stderr().build(); client.initialize_default(); // add the deno.json temp_dir.write("deno.jsonc", r#"{ "imports": { "/~/": "./lib/" } }"#); client.did_change_watched_files(json!({ "changes": [{ "uri": temp_dir.url().join("deno.jsonc").unwrap(), "type": 2 }] })); client.wait_until_stderr_line(|line| { line.contains(" Resolved Deno configuration file:") }); let uri = temp_dir.url().join("a.ts").unwrap(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": uri, "languageId": "typescript", "version": 1, "text": "import { b } from \"/~/b.ts\";\n\nconsole.log(b);\n" } })); assert_eq!(diagnostics.all().len(), 0); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": uri }, "position": { "line": 2, "character": 12 } }), ); assert_eq!( res, json!({ "contents": [ { "language": "typescript", "value":"(alias) const b: \"b\"\nimport b" }, "" ], "range": { "start": { "line": 2, "character": 12 }, "end": { "line": 2, "character": 13 } } }) ); // now cause a syntax error temp_dir.write("deno.jsonc", r#",,#,#,,"#); client.did_change_watched_files(json!({ "changes": [{ "uri": temp_dir.url().join("deno.jsonc").unwrap(), "type": 2 }] })); assert_eq!(client.read_diagnostics().all().len(), 1); // now fix it, and things should work again temp_dir.write("deno.jsonc", r#"{ "imports": { "/~/": "./lib/" } }"#); client.did_change_watched_files(json!({ "changes": [{ "uri": temp_dir.url().join("deno.jsonc").unwrap(), "type": 2 }] })); client.wait_until_stderr_line(|line| { line.contains(" Resolved Deno configuration file:") }); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": uri }, "position": { "line": 2, "character": 12 } }), ); assert_eq!( res, json!({ "contents": [ { "language": "typescript", "value":"(alias) const b: \"b\"\nimport b" }, "" ], "range": { "start": { "line": 2, "character": 12 }, "end": { "line": 2, "character": 13 } } }) ); assert_eq!(client.read_diagnostics().all().len(), 0); client.shutdown(); } #[test] fn lsp_import_map_config_file_auto_discovered_symlink() { let context = TestContextBuilder::new() // DO NOT COPY THIS CODE. Very rare case where we want to force the temp // directory on the CI to not be a symlinked directory because we are // testing a scenario with a symlink to a non-symlink in the same directory // tree. Generally you want to ensure your code works in symlinked directories // so don't use this unless you have a similar scenario. .temp_dir_path(std::env::temp_dir().canonicalize().unwrap()) .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("lib"); temp_dir.write("lib/b.ts", r#"export const b = "b";"#); let mut client = context.new_lsp_command().capture_stderr().build(); client.initialize_default(); // now create a symlink in the current directory to a subdir/deno.json // and ensure the watched files notification still works temp_dir.create_dir_all("subdir"); temp_dir.write("subdir/deno.json", r#"{ }"#); temp_dir.symlink_file( temp_dir.path().join("subdir").join("deno.json"), temp_dir.path().join("deno.json"), ); client.did_change_watched_files(json!({ "changes": [{ // the client will give a watched file changed event for the symlink's target "uri": temp_dir.path().join("subdir/deno.json").canonicalize().url_file(), "type": 2 }] })); // this will discover the deno.json in the root let search_line = format!( " Resolved Deno configuration file: \"{}\"", temp_dir.url().join("deno.json").unwrap().as_str() ); client.wait_until_stderr_line(|line| line.contains(&search_line)); // now open a file which will cause a diagnostic because the import map is empty let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("a.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import { b } from \"/~/b.ts\";\n\nconsole.log(b);\n" } })); assert_eq!(diagnostics.all().len(), 1); // update the import map to have the imports now temp_dir.write("subdir/deno.json", r#"{ "imports": { "/~/": "./lib/" } }"#); client.did_change_watched_files(json!({ "changes": [{ // now still say that the target path has changed "uri": temp_dir.path().join("subdir/deno.json").canonicalize().url_file(), "type": 2 }] })); assert_eq!(client.read_diagnostics().all().len(), 0); client.shutdown(); } #[test] fn lsp_deno_json_imports_comments_cache() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.jsonc", r#"{ // comment "imports": { "print_hello": "http://localhost:4545/import_maps/print_hello.ts", }, }"#, ); temp_dir.write( "file.ts", r#" import { printHello } from "print_hello"; printHello(); "#, ); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], temp_dir.url().join("file.ts").unwrap()], }), ); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": temp_dir.read_to_string("file.ts"), } })); assert_eq!(diagnostics.all(), vec![]); client.shutdown(); } #[test] fn lsp_import_map_node_specifiers() { let context = TestContextBuilder::for_npm().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write("deno.json", r#"{ "imports": { "fs": "node:fs" } }"#); // cache @types/node context .new_command() .args("cache npm:@types/node") .run() .skip_output_check() .assert_exit_code(0); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_config("./deno.json"); }); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("a.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import fs from \"fs\";\nconsole.log(fs);" } })); assert_eq!(diagnostics.all(), vec![]); client.shutdown(); } #[test] fn lsp_format_vendor_path() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); // put this dependency in the global cache context .new_command() .args("cache --allow-import http://localhost:4545/run/002_hello.ts") .run() .skip_output_check(); let temp_dir = context.temp_dir(); temp_dir.write("deno.json", json!({ "vendor": true }).to_string()); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#"import "http://localhost:4545/run/002_hello.ts";"#, }, })); // copying from the global cache to the local cache requires explicitly // running the cache command so that the checksums can be verified assert_eq!( diagnostics .all() .iter() .map(|d| d.message.as_str()) .collect::>(), vec![ "Uncached or missing remote URL: http://localhost:4545/run/002_hello.ts" ] ); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], temp_dir.url().join("file.ts").unwrap()], }), ); assert!(temp_dir .path() .join("vendor/http_localhost_4545/run/002_hello.ts") .exists()); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("vendor/http_localhost_4545/run/002_hello.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#"console.log("Hello World");"#, }, })); let res = client.write_request( "textDocument/formatting", json!({ "textDocument": { "uri": temp_dir.url().join("vendor/http_localhost_4545/run/002_hello.ts").unwrap(), }, "options": { "tabSize": 2, "insertSpaces": true, } }), ); assert_eq!( res, json!([{ "range": { "start": { "line": 0, "character": 27, }, "end": { "line": 0, "character": 27, }, }, "newText": "\n", }]), ); client.shutdown(); } // Regression test for https://github.com/denoland/deno/issues/19802. // Disable the `workspace/configuration` capability. Ensure the LSP falls back // to using `enablePaths` from the `InitializationOptions`. #[test] fn lsp_workspace_enable_paths_no_workspace_configuration() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write("main_disabled.ts", "Date.now()"); temp_dir.write("main_enabled.ts", "Date.now()"); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.with_capabilities(|capabilities| { capabilities.workspace.as_mut().unwrap().configuration = Some(false); }); builder.set_workspace_folders(vec![lsp::WorkspaceFolder { uri: temp_dir.uri(), name: "project".to_string(), }]); builder.set_root_uri(temp_dir.url()); builder.set_enable_paths(vec!["./main_enabled.ts".to_string()]); }); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("main_disabled.ts").unwrap(), "languageId": "typescript", "version": 1, "text": temp_dir.read_to_string("main_disabled.ts"), } })); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("main_enabled.ts").unwrap(), "languageId": "typescript", "version": 1, "text": temp_dir.read_to_string("main_enabled.ts"), } })); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": temp_dir.url().join("main_disabled.ts").unwrap(), }, "position": { "line": 0, "character": 5 } }), ); assert_eq!(res, json!(null)); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": temp_dir.url().join("main_enabled.ts").unwrap(), }, "position": { "line": 0, "character": 5 } }), ); assert_eq!( res, 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": 5, }, "end": { "line": 0, "character": 8, } } }) ); client.shutdown(); } #[test] fn lsp_did_refresh_deno_configuration_tree_notification() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("workspace/member1"); temp_dir.create_dir_all("workspace/member2"); temp_dir.create_dir_all("non_workspace1"); temp_dir.create_dir_all("non_workspace2"); temp_dir.write( "workspace/deno.json", json!({ "workspace": [ "member1", "member2", ], }) .to_string(), ); temp_dir.write("workspace/member1/deno.json", json!({}).to_string()); temp_dir.write("workspace/member1/package.json", json!({}).to_string()); temp_dir.write("workspace/member2/package.json", json!({}).to_string()); temp_dir.write("non_workspace1/deno.json", json!({}).to_string()); let mut client = context.new_lsp_command().build(); client.initialize_default(); let res = client .read_notification_with_method::( "deno/didRefreshDenoConfigurationTree", ) .unwrap(); assert_eq!( res, json!({ "data": [ { "scopeUri": temp_dir.url().join("non_workspace1/").unwrap(), "workspaceRootScopeUri": null, "denoJson": { "uri": temp_dir.url().join("non_workspace1/deno.json").unwrap(), }, "packageJson": null, }, { "scopeUri": temp_dir.url().join("workspace/").unwrap(), "workspaceRootScopeUri": null, "denoJson": { "uri": temp_dir.url().join("workspace/deno.json").unwrap(), }, "packageJson": null, }, { "scopeUri": temp_dir.url().join("workspace/member1/").unwrap(), "workspaceRootScopeUri": temp_dir.url().join("workspace/").unwrap(), "denoJson": { "uri": temp_dir.url().join("workspace/member1/deno.json").unwrap(), }, "packageJson": { "uri": temp_dir.url().join("workspace/member1/package.json").unwrap(), }, }, { "scopeUri": temp_dir.url().join("workspace/member2/").unwrap(), "workspaceRootScopeUri": temp_dir.url().join("workspace/").unwrap(), "denoJson": null, "packageJson": { "uri": temp_dir.url().join("workspace/member2/package.json").unwrap(), }, }, ], }), ); temp_dir.write("non_workspace2/deno.json", json!({}).to_string()); client.did_change_watched_files(json!({ "changes": [{ "uri": temp_dir.url().join("non_workspace2/deno.json").unwrap(), "type": 1, }], })); let res = client .read_notification_with_method::( "deno/didRefreshDenoConfigurationTree", ) .unwrap(); assert_eq!( res, json!({ "data": [ { "scopeUri": temp_dir.url().join("non_workspace1/").unwrap(), "workspaceRootScopeUri": null, "denoJson": { "uri": temp_dir.url().join("non_workspace1/deno.json").unwrap(), }, "packageJson": null, }, { "scopeUri": temp_dir.url().join("non_workspace2/").unwrap(), "workspaceRootScopeUri": null, "denoJson": { "uri": temp_dir.url().join("non_workspace2/deno.json").unwrap(), }, "packageJson": null, }, { "scopeUri": temp_dir.url().join("workspace/").unwrap(), "workspaceRootScopeUri": null, "denoJson": { "uri": temp_dir.url().join("workspace/deno.json").unwrap(), }, "packageJson": null, }, { "scopeUri": temp_dir.url().join("workspace/member1/").unwrap(), "workspaceRootScopeUri": temp_dir.url().join("workspace/").unwrap(), "denoJson": { "uri": temp_dir.url().join("workspace/member1/deno.json").unwrap(), }, "packageJson": { "uri": temp_dir.url().join("workspace/member1/package.json").unwrap(), }, }, { "scopeUri": temp_dir.url().join("workspace/member2/").unwrap(), "workspaceRootScopeUri": temp_dir.url().join("workspace/").unwrap(), "denoJson": null, "packageJson": { "uri": temp_dir.url().join("workspace/member2/package.json").unwrap(), }, }, ], }), ); client.change_configuration(json!({ "deno": { "disablePaths": ["non_workspace1"], }, })); let res = client .read_notification_with_method::( "deno/didRefreshDenoConfigurationTree", ) .unwrap(); assert_eq!( res, json!({ "data": [ { "scopeUri": temp_dir.url().join("non_workspace2/").unwrap(), "workspaceRootScopeUri": null, "denoJson": { "uri": temp_dir.url().join("non_workspace2/deno.json").unwrap(), }, "packageJson": null, }, { "scopeUri": temp_dir.url().join("workspace/").unwrap(), "workspaceRootScopeUri": null, "denoJson": { "uri": temp_dir.url().join("workspace/deno.json").unwrap(), }, "packageJson": null, }, { "scopeUri": temp_dir.url().join("workspace/member1/").unwrap(), "workspaceRootScopeUri": temp_dir.url().join("workspace/").unwrap(), "denoJson": { "uri": temp_dir.url().join("workspace/member1/deno.json").unwrap(), }, "packageJson": { "uri": temp_dir.url().join("workspace/member1/package.json").unwrap(), }, }, { "scopeUri": temp_dir.url().join("workspace/member2/").unwrap(), "workspaceRootScopeUri": temp_dir.url().join("workspace/").unwrap(), "denoJson": null, "packageJson": { "uri": temp_dir.url().join("workspace/member2/package.json").unwrap(), }, }, ], }), ); client.shutdown(); } #[test] fn lsp_did_change_deno_configuration_notification() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write("deno.json", json!({}).to_string()); temp_dir.write("package.json", json!({}).to_string()); let mut client = context.new_lsp_command().build(); client.initialize_default(); let res = client .read_notification_with_method::("deno/didChangeDenoConfiguration"); assert_eq!( res, Some(json!({ "changes": [ { "scopeUri": temp_dir.url(), "fileUri": temp_dir.url().join("deno.json").unwrap(), "type": "added", "configurationType": "denoJson" }, { "scopeUri": temp_dir.url(), "fileUri": temp_dir.url().join("package.json").unwrap(), "type": "added", "configurationType": "packageJson" }, ], })) ); temp_dir.write( "deno.json", json!({ "fmt": { "semiColons": false } }).to_string(), ); client.did_change_watched_files(json!({ "changes": [{ "uri": temp_dir.url().join("deno.json").unwrap(), "type": 2, }], })); let res = client .read_notification_with_method::("deno/didChangeDenoConfiguration"); assert_eq!( res, Some(json!({ "changes": [{ "scopeUri": temp_dir.url(), "fileUri": temp_dir.url().join("deno.json").unwrap(), "type": "changed", "configurationType": "denoJson" }], })) ); temp_dir.remove_file("deno.json"); client.did_change_watched_files(json!({ "changes": [{ "uri": temp_dir.url().join("deno.json").unwrap(), "type": 3, }], })); let res = client .read_notification_with_method::("deno/didChangeDenoConfiguration"); assert_eq!( res, Some(json!({ "changes": [{ "scopeUri": temp_dir.url(), "fileUri": temp_dir.url().join("deno.json").unwrap(), "type": "removed", "configurationType": "denoJson" }], })) ); temp_dir.write("package.json", json!({ "type": "module" }).to_string()); client.did_change_watched_files(json!({ "changes": [{ "uri": temp_dir.url().join("package.json").unwrap(), "type": 2, }], })); let res = client .read_notification_with_method::("deno/didChangeDenoConfiguration"); assert_eq!( res, Some(json!({ "changes": [{ "scopeUri": temp_dir.url(), "fileUri": temp_dir.url().join("package.json").unwrap(), "type": "changed", "configurationType": "packageJson" }], })) ); temp_dir.remove_file("package.json"); client.did_change_watched_files(json!({ "changes": [{ "uri": temp_dir.url().join("package.json").unwrap(), "type": 3, }], })); let res = client .read_notification_with_method::("deno/didChangeDenoConfiguration"); assert_eq!( res, Some(json!({ "changes": [{ "scopeUri": temp_dir.url(), "fileUri": temp_dir.url().join("package.json").unwrap(), "type": "removed", "configurationType": "packageJson" }], })) ); client.shutdown(); } #[test] fn lsp_deno_task() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.jsonc", json!({ "tasks": { "build": "deno test", "serve": { "description": "Start the dev server", "command": "deno run -RN server.ts", }, }, }) .to_string(), ); let mut client = context.new_lsp_command().build(); client.initialize_default(); let res = client.write_request("deno/taskDefinitions", json!(null)); assert_eq!( res, json!([ { "name": "build", "command": "deno test", "sourceUri": temp_dir.url().join("deno.jsonc").unwrap(), }, { "name": "serve", "command": "deno run -RN server.ts", "sourceUri": temp_dir.url().join("deno.jsonc").unwrap(), } ]) ); client.shutdown(); } #[test] fn lsp_reload_import_registries_command() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let res = client.write_request( "workspace/executeCommand", json!({ "command": "deno.reloadImportRegistries" }), ); assert_eq!(res, json!(true)); client.shutdown(); } #[test] fn lsp_import_attributes() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_import_map("data:application/json;utf8,{\"imports\": { \"example\": \"https://deno.land/x/example/mod.ts\" }}"); }); client.change_configuration(json!({ "deno": { "enable": true, "codeLens": { "test": true, }, }, })); client.did_open(json!({ "textDocument": { "uri": "file:///a/test.json", "languageId": "json", "version": 1, "text": "{\"a\":1}", }, })); let diagnostics = client.did_open(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 .messages_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-attribute-type", "source": "deno", "message": "The module is a JSON module and not being imported with an import attribute. Consider adding `with { type: \"json\" }` to the import statement." } ]) ); let res = client .write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": "file:///a/a.ts" }, "range": { "start": { "line": 0, "character": 14 }, "end": { "line": 0, "character": 27 } }, "context": { "diagnostics": [{ "range": { "start": { "line": 0, "character": 14 }, "end": { "line": 0, "character": 27 } }, "severity": 1, "code": "no-attribute-type", "source": "deno", "message": "The module is a JSON module and not being imported with an import attribute. Consider adding `with { type: \"json\" }` to the import statement." }], "only": ["quickfix"] } }), ); assert_eq!( res, json!([{ "title": "Insert import attribute.", "kind": "quickfix", "diagnostics": [ { "range": { "start": { "line": 0, "character": 14 }, "end": { "line": 0, "character": 27 } }, "severity": 1, "code": "no-attribute-type", "source": "deno", "message": "The module is a JSON module and not being imported with an import attribute. Consider adding `with { type: \"json\" }` to the import statement." } ], "edit": { "changes": { "file:///a/a.ts": [ { "range": { "start": { "line": 0, "character": 27 }, "end": { "line": 0, "character": 27 } }, "newText": " with { type: \"json\" }" } ] } } }]) ); client.shutdown(); } #[test] fn lsp_import_map_import_completions() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.json", json!({ "imports": { "/~/": "./lib/", "/#/": "./src/", "fs": "https://example.com/fs/index.js", "std/": "https://example.com/std@0.123.0/", "lib/": "./lib/", }, "scopes": { "file:///": { "file": "./file.ts", }, }, }) .to_string(), ); temp_dir.create_dir_all("lib"); temp_dir.write("lib/b.ts", r#"export const b = "b";"#); let mut client = context.new_lsp_command().build(); client.initialize_default(); let uri = temp_dir.url().join("a.ts").unwrap(); client.did_open(json!({ "textDocument": { "uri": uri, "languageId": "typescript", "version": 1, "text": r#" import * as b from ""; import * as b from "/~/"; import * as b from "lib/"; "#, }, })); let res = client.get_completion( &uri, (1, 28), json!({ "triggerKind": 2, "triggerCharacter": "\"" }), ); assert_eq!( json!(res), json!({ "isIncomplete": false, "items": [ { "label": ".", "kind": 19, "detail": "(local)", "sortText": "1", "insertText": ".", "commitCharacters": ["\"", "'"], }, { "label": "..", "kind": 19, "detail": "(local)", "sortText": "1", "insertText": "..", "commitCharacters": ["\"", "'"], }, { "label": "file", "kind": 17, "detail": "(import map)", "sortText": "file", "insertText": "file", "commitCharacters": ["\"", "'"], }, { "label": "std", "kind": 19, "detail": "(import map)", "sortText": "std", "insertText": "std", "commitCharacters": ["\"", "'"], }, { "label": "lib", "kind": 19, "detail": "(import map)", "sortText": "lib", "insertText": "lib", "commitCharacters": ["\"", "'"], }, { "label": "fs", "kind": 17, "detail": "(import map)", "sortText": "fs", "insertText": "fs", "commitCharacters": ["\"", "'"], }, { "label": "/~", "kind": 19, "detail": "(import map)", "sortText": "/~", "insertText": "/~", "commitCharacters": ["\"", "'"], }, { "label": "/#", "kind": 19, "detail": "(import map)", "sortText": "/#", "insertText": "/#", "commitCharacters": ["\"", "'"], }, ] }) ); let res = client.get_completion( &uri, (2, 31), json!({ "triggerKind": 2, "triggerCharacter": "/" }), ); assert_eq!( json!(res), json!({ "isIncomplete": false, "items": [ { "label": "b.ts", "kind": 17, "detail": "(local)", "sortText": "1", "filterText": "/~/b.ts", "textEdit": { "range": { "start": { "line": 2, "character": 28 }, "end": { "line": 2, "character": 31 }, }, "newText": "/~/b.ts", }, "commitCharacters": ["\"", "'"], }, ], }), ); let res = client.get_completion( &uri, (3, 32), json!({ "triggerKind": 2, "triggerCharacter": "/" }), ); assert_eq!( json!(res), json!({ "isIncomplete": false, "items": [ { "label": "b.ts", "kind": 17, "detail": "(local)", "sortText": "1", "filterText": "lib/b.ts", "textEdit": { "range": { "start": { "line": 3, "character": 28 }, "end": { "line": 3, "character": 32 }, }, "newText": "lib/b.ts", }, "commitCharacters": ["\"", "'"], }, ], }), ); client.shutdown(); } #[test] fn lsp_hover() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "console.log(Deno.args);\n" } })); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 0, "character": 19 } }), ); assert_eq!( res, 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://examples.deno.land/command-line-arguments.ts Sushi\n```\n\nThen `Deno.args` will contain:\n\n```ts\n[ \"Sushi\" ]\n```\n\nIf you are looking for a structured way to parse arguments, there is\n[`parseArgs()`](https://jsr.io/@std/cli/doc/parse-args/~/parseArgs) from\nthe Deno Standard Library.", "\n\n*@category* - Runtime", ], "range": { "start": { "line": 0, "character": 17 }, "end": { "line": 0, "character": 21 } } }) ); client.shutdown(); } #[test] fn lsp_hover_asset() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write("deno.json", json!({}).to_string()); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "console.log(Date.now());\n" } })); client.write_request( "textDocument/definition", json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap() }, "position": { "line": 0, "character": 14 } }), ); client.write_request( "deno/virtualTextDocument", json!({ "textDocument": { "uri": "deno:/asset/lib.deno.shared_globals.d.ts" } }), ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "deno:/asset/lib.es2015.symbol.wellknown.d.ts" }, "position": { "line": 111, "character": 13 } }), ); assert_eq!( res, json!({ "contents": [ { "language": "typescript", "value": "interface Date", }, "Enables basic storage and retrieval of dates and times.", "\n\n*@category* - Temporal \n\n*@experimental*" ], "range": { "start": { "line": 111, "character": 10, }, "end": { "line": 111, "character": 14, } } }) ); client.shutdown(); } #[test] fn lsp_hover_disabled() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_deno_enable(false); }); client.change_configuration(json!({ "deno": { "enable": false } })); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "console.log(Date.now());\n", }, })); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 0, "character": 19 } }), ); assert_eq!(res, json!(null)); client.shutdown(); } #[test] fn lsp_inlay_hints() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.change_configuration(json!({ "deno": { "enable": true, }, "typescript": { "inlayHints": { "parameterNames": { "enabled": "all", }, "parameterTypes": { "enabled": true, }, "variableTypes": { "enabled": true, }, "propertyDeclarationTypes": { "enabled": true, }, "functionLikeReturnTypes": { "enabled": true, }, "enumMemberValues": { "enabled": true, }, }, }, })); client.did_open(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); interface Bar { someField: string; } function getBar(): Bar { return { someField: "foo" }; } // This shouldn't have a type hint because the variable name makes it // redundant. const bar = getBar(); const someValue = getBar(); "#, }, })); let res = client.write_request( "textDocument/inlayHint", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "range": { "start": { "line": 1, "character": 0 }, "end": { "line": 31, "character": 0, }, }, }), ); assert_eq!( res, json!([ { "position": { "line": 1, "character": 29 }, "label": [{ "value": ": " }, { "value": "string" }], "kind": 1, "paddingLeft": true, }, { "position": { "line": 5, "character": 10 }, "label": [ { "value": "b", "location": { "uri": "file:///a/file.ts", "range": { "start": { "line": 1, "character": 19 }, "end": { "line": 1, "character": 20 }, }, }, }, { "value": ":" }, ], "kind": 2, "paddingRight": true, }, { "position": { "line": 8, "character": 11 }, "label": "= 0", "paddingLeft": true, }, { "position": { "line": 11, "character": 17 }, "label": [ { "value": "string", "location": { "uri": "deno:/asset/lib.es5.d.ts", "range": { "start": { "line": 41, "character": 26 }, "end": { "line": 41, "character": 32 }, }, }, }, { "value": ":" }, ], "kind": 2, "paddingRight": true, }, { "position": { "line": 11, "character": 24 }, "label": [ { "value": "radix", "location": { "uri": "deno:/asset/lib.es5.d.ts", "range": { "start": { "line": 41, "character": 42 }, "end": { "line": 41, "character": 47 }, }, }, }, { "value": ":" }, ], "kind": 2, "paddingRight": true, }, { "position": { "line": 13, "character": 15 }, "label": [{ "value": ": " }, { "value": "number" }], "kind": 1, "paddingLeft": true, }, { "position": { "line": 16, "character": 11 }, "label": [{ "value": ": " }, { "value": "number" }], "kind": 1, "paddingLeft": true, }, { "position": { "line": 19, "character": 18 }, "label": [ { "value": "callbackfn", "location": { "uri": "deno:/asset/lib.es5.d.ts", "range": { "start": { "line": 1462, "character": 11 }, "end": { "line": 1462, "character": 21 }, }, }, }, { "value": ":" }, ], "kind": 2, "paddingRight": true, }, { "position": { "line": 19, "character": 20 }, "label": [{ "value": ": " }, { "value": "string" }], "kind": 1, "paddingLeft": true, }, { "position": { "line": 19, "character": 21 }, "label": [{ "value": ": " }, { "value": "string" }], "kind": 1, "paddingLeft": true, }, { "position": { "line": 30, "character": 23 }, "label": [ { "value": ": " }, { "value": "Bar", "location": { "uri": "file:///a/file.ts", "range": { "start": { "line": 21, "character": 18 }, "end": { "line": 21, "character": 21 }, }, }, }, ], "kind": 1, "paddingLeft": true, }, ]), ); client.shutdown(); } #[test] fn lsp_inlay_hints_not_enabled() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(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 res = client.write_request( "textDocument/inlayHint", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 19, "character": 0, } } }), ); assert_eq!(res, json!(null)); client.shutdown(); } #[test] fn lsp_workspace_disable_enable_paths() { fn run_test(use_trailing_slash: bool) { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("worker"); temp_dir.write("worker/shared.ts", "export const a = 1"); temp_dir.write("worker/other.ts", "import { a } from './shared.ts';\na;"); temp_dir.write("worker/node.ts", "Buffer.alloc(1);"); let root_specifier = temp_dir.url(); let mut client = context.new_lsp_command().build(); client.initialize_with_config( |builder| { builder .set_disable_paths(vec!["./worker/node.ts".to_string()]) .set_enable_paths(vec!["./worker".to_string()]) .set_root_uri(root_specifier.clone()) .set_workspace_folders(vec![lsp::WorkspaceFolder { uri: if use_trailing_slash { lsp::Uri::from_str(root_specifier.as_str()).unwrap() } else { lsp::Uri::from_str( root_specifier.as_str().strip_suffix('/').unwrap(), ) .unwrap() }, name: "project".to_string(), }]); }, json!({ "deno": { "disablePaths": ["./worker/node.ts"], "enablePaths": ["./worker"], } }), ); client.did_open(json!({ "textDocument": { "uri": root_specifier.join("./file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "console.log(Date.now());\n" } })); client.did_open(json!({ "textDocument": { "uri": root_specifier.join("./other/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "console.log(Date.now());\n" } })); client.did_open(json!({ "textDocument": { "uri": root_specifier.join("./worker/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": concat!( "console.log(Date.now());\n", "import { a } from './shared.ts';\n", "a;\n", ), } })); client.did_open(json!({ "textDocument": { "uri": root_specifier.join("./worker/subdir/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "console.log(Date.now());\n" } })); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": root_specifier.join("./file.ts").unwrap(), }, "position": { "line": 0, "character": 19 } }), ); assert_eq!(res, json!(null)); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": root_specifier.join("./other/file.ts").unwrap(), }, "position": { "line": 0, "character": 19 } }), ); assert_eq!(res, json!(null)); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": root_specifier.join("./worker/node.ts").unwrap(), }, "position": { "line": 0, "character": 0 } }), ); assert_eq!(res, json!(null)); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": root_specifier.join("./worker/file.ts").unwrap(), }, "position": { "line": 0, "character": 19 } }), ); assert_eq!( res, 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 res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": root_specifier.join("./worker/subdir/file.ts").unwrap(), }, "position": { "line": 0, "character": 19 } }), ); assert_eq!( res, 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, } } }) ); // check that the file system documents were auto-discovered // via the enabled paths let res = client.write_request( "textDocument/references", json!({ "textDocument": { "uri": root_specifier.join("./worker/file.ts").unwrap(), }, "position": { "line": 2, "character": 0 }, "context": { "includeDeclaration": true } }), ); assert_eq!( res, json!([{ "uri": root_specifier.join("./worker/file.ts").unwrap(), "range": { "start": { "line": 1, "character": 9 }, "end": { "line": 1, "character": 10 } } }, { "uri": root_specifier.join("./worker/file.ts").unwrap(), "range": { "start": { "line": 2, "character": 0 }, "end": { "line": 2, "character": 1 } } }, { "uri": root_specifier.join("./worker/shared.ts").unwrap(), "range": { "start": { "line": 0, "character": 13 }, "end": { "line": 0, "character": 14 } } }, { "uri": root_specifier.join("./worker/other.ts").unwrap(), "range": { "start": { "line": 0, "character": 9 }, "end": { "line": 0, "character": 10 } } }, { "uri": root_specifier.join("./worker/other.ts").unwrap(), "range": { "start": { "line": 1, "character": 0 }, "end": { "line": 1, "character": 1 } } }]) ); client.shutdown(); } run_test(true); run_test(false); } #[test] fn lsp_exclude_config() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("other"); temp_dir.write( "other/shared.ts", // this should not be found in the "find references" since this file is excluded "import { a } from '../worker/shared.ts'; console.log(a);", ); temp_dir.create_dir_all("worker"); temp_dir.write("worker/shared.ts", "export const a = 1"); temp_dir.write( "deno.json", r#"{ "exclude": ["other"], }"#, ); let root_specifier = temp_dir.url(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": root_specifier.join("./other/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "console.log(Date.now());\n" } })); client.did_open(json!({ "textDocument": { "uri": root_specifier.join("./worker/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": concat!( "console.log(Date.now());\n", "import { a } from './shared.ts';\n", "a;\n", ), } })); client.did_open(json!({ "textDocument": { "uri": root_specifier.join("./worker/subdir/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "console.log(Date.now());\n" } })); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": root_specifier.join("./other/file.ts").unwrap(), }, "position": { "line": 0, "character": 19 } }), ); assert_eq!(res, json!(null)); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": root_specifier.join("./worker/file.ts").unwrap(), }, "position": { "line": 0, "character": 19 } }), ); assert_eq!( res, 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, } } }) ); // check that the file system documents were auto-discovered let res = client.write_request( "textDocument/references", json!({ "textDocument": { "uri": root_specifier.join("./worker/file.ts").unwrap(), }, "position": { "line": 2, "character": 0 }, "context": { "includeDeclaration": true } }), ); assert_eq!( res, json!([{ "uri": root_specifier.join("./worker/file.ts").unwrap(), "range": { "start": { "line": 1, "character": 9 }, "end": { "line": 1, "character": 10 } } }, { "uri": root_specifier.join("./worker/file.ts").unwrap(), "range": { "start": { "line": 2, "character": 0 }, "end": { "line": 2, "character": 1 } } }, { "uri": root_specifier.join("./worker/shared.ts").unwrap(), "range": { "start": { "line": 0, "character": 13 }, "end": { "line": 0, "character": 14 } } }]) ); client.shutdown(); } #[test] fn lsp_hover_unstable_always_enabled() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, // IMPORTANT: If you change this API due to stabilization, also change it // in the enabled test below. "text": "type _ = Deno.DatagramConn;\n" } })); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 0, "character": 14 } }), ); assert_eq!( res, json!({ "contents":[ { "language":"typescript", "value":"interface Deno.DatagramConn" }, "**UNSTABLE**: New API, yet to be vetted.\n\nA generic transport listener for message-oriented protocols.", "\n\n*@category* - Network \n\n*@experimental*", ], "range":{ "start":{ "line":0, "character":14 }, "end":{ "line":0, "character":26 } } }) ); client.shutdown(); } #[test] fn lsp_hover_unstable_enabled() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { // NOTE(bartlomieju): this is effectively not used anymore. builder.set_unstable(true); }); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "type _ = Deno.DatagramConn;\n" } })); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 0, "character": 14 } }), ); assert_eq!( res, json!({ "contents":[ { "language":"typescript", "value":"interface Deno.DatagramConn" }, "**UNSTABLE**: New API, yet to be vetted.\n\nA generic transport listener for message-oriented protocols.", "\n\n*@category* - Network \n\n*@experimental*", ], "range":{ "start":{ "line":0, "character":14 }, "end":{ "line":0, "character":26 } } }) ); client.shutdown(); } #[test] fn lsp_hover_change_mbc() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( 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 emoji is character index 15 "character": 15 } }, "text": "" } ] }), ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 2, "character": 15 } }), ); assert_eq!( res, json!({ "contents": [ { "language": "typescript", "value": "const b: \"😃\"", }, "", ], "range": { "start": { "line": 2, "character": 15, }, "end": { "line": 2, "character": 16, }, } }) ); client.shutdown(); } #[test] fn lsp_hover_closed_document() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write("a.ts", r#"export const a = "a";"#); temp_dir.write("b.ts", r#"export * from "./a.ts";"#); temp_dir.write("c.ts", "import { a } from \"./b.ts\";\nconsole.log(a);\n"); let b_specifier = temp_dir.url().join("b.ts").unwrap(); let c_specifier = temp_dir.url().join("c.ts").unwrap(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": b_specifier, "languageId": "typescript", "version": 1, "text": r#"export * from "./a.ts";"# } })); client.did_open(json!({ "textDocument": { "uri": c_specifier, "languageId": "typescript", "version": 1, "text": "import { a } from \"./b.ts\";\nconsole.log(a);\n", } })); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": c_specifier, }, "position": { "line": 0, "character": 10 } }), ); assert_eq!( res, 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, } }), ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": c_specifier, }, "position": { "line": 0, "character": 10 } }), ); assert_eq!( res, json!({ "contents": [ { "language": "typescript", "value": "(alias) const a: \"a\"\nimport a" }, "" ], "range": { "start": { "line": 0, "character": 9 }, "end": { "line": 0, "character": 10 } } }) ); client.shutdown(); } #[test] fn lsp_hover_dependency() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file_01.ts", "languageId": "typescript", "version": 1, "text": "export const a = \"a\";\n", } })); client.did_open( 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// @deno-types=\"http://127.0.0.1:4545/type_definitions/foo.d.ts\"\nimport * as b from \"http://127.0.0.1:4545/type_definitions/foo.js\";\nimport * as c from \"http://127.0.0.1:4545/subdir/type_reference.js\";\nimport * as d from \"http://127.0.0.1:4545/subdir/mod1.ts\";\nimport * as e from \"data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=\";\nimport * as f from \"./file_01.ts\";\nimport * as g from \"http://localhost:4545/x/a/mod.ts\";\nimport * as h from \"./mod🦕.ts\";\n\nconsole.log(a, b, c, d, e, f, g, h);\n" } }), ); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], "file:///a/file.ts"], }), ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "position": { "line": 0, "character": 28 } }), ); assert_eq!( res, 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 res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "position": { "line": 3, "character": 28 } }), ); assert_eq!( res, 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 res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "position": { "line": 4, "character": 28 } }), ); assert_eq!( res, 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 res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "position": { "line": 5, "character": 28 } }), ); assert_eq!( res, 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 res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "position": { "line": 6, "character": 28 } }), ); assert_eq!( res, 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 } } }) ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "position": { "line": 8, "character": 28 } }), ); assert_eq!( res, json!({ "contents": { "kind": "markdown", "value": "**Resolved Dependency**\n\n**Code**: file​:///a/mod🦕.ts\n" }, "range": { "start": { "line": 8, "character": 19 }, "end":{ "line": 8, "character": 30 } } }) ); client.shutdown(); } // 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 context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file1.ts", "languageId": "typescript", "version": 1, "text": "export type Foo = { bar(): string };\n" } })); client.did_open(json!({ "textDocument": { "uri": "file:///a/file2.ts", "languageId": "typescript", "version": 1, "text": "import { Foo } from './file1.ts'; declare const f: Foo; f\n" } })); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file2.ts" }, "position": { "line": 0, "character": 56 } }), ); assert_eq!( res, 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": "." } ] }), ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file2.ts" }, "position": { "line": 0, "character": 56 } }), ); assert_eq!( res, json!({ "contents": [ { "language": "typescript", "value": "const f: Foo", }, "" ], "range": { "start": { "line": 0, "character": 56, }, "end": { "line": 0, "character": 57, } } }) ); client.shutdown(); } // Regression test for https://github.com/denoland/vscode_deno/issues/1068. #[test] fn lsp_rename_synbol_file_scheme_edits_only() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#" import { SEPARATOR } from "http://localhost:4545/subdir/exports.ts"; console.log(SEPARATOR); "#, }, })); let res = client.write_request( "textDocument/rename", json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), }, "position": { "line": 1, "character": 17 }, "newName": "PATH_SEPARATOR", }), ); assert_eq!( res, json!({ "documentChanges": [ { "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "version": 1, }, "edits": [ { "range": { "start": { "line": 1, "character": 17 }, "end": { "line": 1, "character": 26 }, }, "newText": "PATH_SEPARATOR", }, { "range": { "start": { "line": 2, "character": 20 }, "end": { "line": 2, "character": 29 }, }, "newText": "PATH_SEPARATOR", }, ], } ], }) ); client.shutdown(); } // Regression test for https://github.com/denoland/deno/issues/23121. #[test] fn lsp_document_preload_limit_zero_deno_json_detection() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.write("deno.json", json!({}).to_string()); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_preload_limit(0); }); let res = client .read_notification_with_method::("deno/didChangeDenoConfiguration"); assert_eq!( res, Some(json!({ "changes": [{ "scopeUri": temp_dir.url(), "fileUri": temp_dir.url().join("deno.json").unwrap(), "type": "added", "configurationType": "denoJson", }], })) ); client.shutdown(); } // Regression test for https://github.com/denoland/deno/issues/23141. #[test] fn lsp_import_map_setting_with_deno_json() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.write("deno.json", json!({}).to_string()); temp_dir.write( "import_map.json", json!({ "imports": { "file2": "./file2.ts", }, }) .to_string(), ); temp_dir.write("file2.ts", ""); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_import_map("import_map.json"); }); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import \"file2\";\n", }, })); assert_eq!(json!(diagnostics.all()), json!([])); client.shutdown(); } #[test] fn lsp_hover_typescript_types() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( 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", } }), ); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [ ["http://127.0.0.1:4545/xTypeScriptTypes.js"], "file:///a/file.ts", ], }), ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 0, "character": 24 } }), ); assert_eq!( res, 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 } } }) ); client.shutdown(); } #[test] fn lsp_hover_jsr() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import \"jsr:@denotest/add@1.0.0\";\n", } })); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], temp_dir.url().join("file.ts").unwrap()], }), ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), }, "position": { "line": 0, "character": 7 }, }), ); assert_eq!( res, json!({ "contents": { "kind": "markdown", "value": "**Resolved Dependency**\n\n**Code**: jsr​:​@denotest/add​@1.0.0 ()\n", }, "range": { "start": { "line": 0, "character": 7 }, "end": { "line": 0, "character": 32 }, }, }), ); client.shutdown(); } #[test] fn lsp_hover_jsdoc_symbol_link() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/b.ts", "languageId": "typescript", "version": 1, "text": "export function hello() {}\n" } })); client.did_open( 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 res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 7, "character": 10 } }), ); assert_eq!( res, json!({ "contents": [ { "language": "typescript", "value": "function a(): void" }, "JSDoc [hello](file:///a/b.ts#L1,1) and [`b`](file:///a/file.ts#L5,7)" ], "range": { "start": { "line": 7, "character": 9 }, "end": { "line": 7, "character": 10 } } }) ); client.shutdown(); } #[test] fn lsp_goto_type_definition() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( 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 res = client.write_request( "textDocument/typeDefinition", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 12, "character": 1 } }), ); assert_eq!( res, 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 } } } ]) ); client.shutdown(); } #[test] fn lsp_call_hierarchy() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( 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 res = client.write_request( "textDocument/prepareCallHierarchy", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 5, "character": 3 } }), ); assert_eq!( res, json!([{ "name": "baz", "kind": 6, "detail": "Bar", "uri": "file:///a/file.ts", "range": { "start": { "line": 5, "character": 2 }, "end": { "line": 7, "character": 3 } }, "selectionRange": { "start": { "line": 5, "character": 2 }, "end": { "line": 5, "character": 5 } } }]) ); let res = client.write_request( "callHierarchy/incomingCalls", json!({ "item": { "name": "baz", "kind": 6, "detail": "Bar", "uri": "file:///a/file.ts", "range": { "start": { "line": 5, "character": 2 }, "end": { "line": 7, "character": 3 } }, "selectionRange": { "start": { "line": 5, "character": 2 }, "end": { "line": 5, "character": 5 } } } }), ); assert_eq!( res, json!([{ "from": { "name": "main", "kind": 12, "detail": "", "uri": "file:///a/file.ts", "range": { "start": { "line": 10, "character": 0 }, "end": { "line": 13, "character": 1 } }, "selectionRange": { "start": { "line": 10, "character": 9 }, "end": { "line": 10, "character": 13 } } }, "fromRanges": [ { "start": { "line": 12, "character": 6 }, "end": { "line": 12, "character": 9 } } ] }]) ); let res = client.write_request( "callHierarchy/outgoingCalls", json!({ "item": { "name": "baz", "kind": 6, "detail": "Bar", "uri": "file:///a/file.ts", "range": { "start": { "line": 5, "character": 2 }, "end": { "line": 7, "character": 3 } }, "selectionRange": { "start": { "line": 5, "character": 2 }, "end": { "line": 5, "character": 5 } } } }), ); assert_eq!( res, json!([{ "to": { "name": "foo", "kind": 12, "detail": "", "uri": "file:///a/file.ts", "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 2, "character": 1 } }, "selectionRange": { "start": { "line": 0, "character": 9 }, "end": { "line": 0, "character": 12 } } }, "fromRanges": [{ "start": { "line": 6, "character": 11 }, "end": { "line": 6, "character": 14 } }] }]) ); client.shutdown(); } #[test] fn lsp_large_doc_changes() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let large_file_text = fs::read_to_string(testdata_path().join("lsp").join("large_file.txt")) .unwrap(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "javascript", "version": 1, "text": large_file_text, } })); 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": "+++" } ] }), ); 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": "// " } ] }), ); 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" } ] }), ); client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 421, "character": 30 } }), ); client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 444, "character": 6 } }), ); client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 461, "character": 34 } }), ); client.shutdown(); assert!(client.duration().as_millis() <= 15000); } #[test] fn lsp_document_symbol() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "interface IFoo {\n foo(): boolean;\n}\n\nclass Bar implements IFoo {\n constructor(public x: number) { }\n foo() { return true; }\n /** @deprecated */\n baz() { return false; }\n get value(): number { return 0; }\n set value(_newValue: number) { return; }\n static staticBar = new Bar(0);\n private static getStaticBar() { return Bar.staticBar; }\n}\n\nenum Values { value1, value2 }\n\nvar bar: IFoo = new Bar(3);" } }), ); let res = client.write_request( "textDocument/documentSymbol", json!({ "textDocument": { "uri": "file:///a/file.ts" } }), ); assert_eq!( res, json!([{ "name": "bar", "kind": 13, "range": { "start": { "line": 17, "character": 4 }, "end": { "line": 17, "character": 26 } }, "selectionRange": { "start": { "line": 17, "character": 4 }, "end": { "line": 17, "character": 7 } } }, { "name": "Bar", "kind": 5, "range": { "start": { "line": 4, "character": 0 }, "end": { "line": 13, "character": 1 } }, "selectionRange": { "start": { "line": 4, "character": 6 }, "end": { "line": 4, "character": 9 } }, "children": [{ "name": "constructor", "kind": 9, "range": { "start": { "line": 5, "character": 2 }, "end": { "line": 5, "character": 35 } }, "selectionRange": { "start": { "line": 5, "character": 2 }, "end": { "line": 5, "character": 35 } } }, { "name": "baz", "kind": 6, "tags": [1], "range": { "start": { "line": 8, "character": 2 }, "end": { "line": 8, "character": 25 } }, "selectionRange": { "start": { "line": 8, "character": 2 }, "end": { "line": 8, "character": 5 } } }, { "name": "foo", "kind": 6, "range": { "start": { "line": 6, "character": 2 }, "end": { "line": 6, "character": 24 } }, "selectionRange": { "start": { "line": 6, "character": 2 }, "end": { "line": 6, "character": 5 } } }, { "name": "getStaticBar", "kind": 6, "range": { "start": { "line": 12, "character": 2 }, "end": { "line": 12, "character": 57 } }, "selectionRange": { "start": { "line": 12, "character": 17 }, "end": { "line": 12, "character": 29 } } }, { "name": "staticBar", "kind": 8, "range": { "start": { "line": 11, "character": 2 }, "end": { "line": 11, "character": 32 } }, "selectionRange": { "start": { "line": 11, "character": 9 }, "end": { "line": 11, "character": 18 } } }, { "name": "(get) value", "kind": 8, "range": { "start": { "line": 9, "character": 2 }, "end": { "line": 9, "character": 35 } }, "selectionRange": { "start": { "line": 9, "character": 6 }, "end": { "line": 9, "character": 11 } } }, { "name": "(set) value", "kind": 8, "range": { "start": { "line": 10, "character": 2 }, "end": { "line": 10, "character": 42 } }, "selectionRange": { "start": { "line": 10, "character": 6 }, "end": { "line": 10, "character": 11 } } }, { "name": "x", "kind": 8, "range": { "start": { "line": 5, "character": 14 }, "end": { "line": 5, "character": 30 } }, "selectionRange": { "start": { "line": 5, "character": 21 }, "end": { "line": 5, "character": 22 } } }] }, { "name": "IFoo", "kind": 11, "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 2, "character": 1 } }, "selectionRange": { "start": { "line": 0, "character": 10 }, "end": { "line": 0, "character": 14 } }, "children": [{ "name": "foo", "kind": 6, "range": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 17 } }, "selectionRange": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 5 } } }] }, { "name": "Values", "kind": 10, "range": { "start": { "line": 15, "character": 0 }, "end": { "line": 15, "character": 30 } }, "selectionRange": { "start": { "line": 15, "character": 5 }, "end": { "line": 15, "character": 11 } }, "children": [{ "name": "value1", "kind": 22, "range": { "start": { "line": 15, "character": 14 }, "end": { "line": 15, "character": 20 } }, "selectionRange": { "start": { "line": 15, "character": 14 }, "end": { "line": 15, "character": 20 } } }, { "name": "value2", "kind": 22, "range": { "start": { "line": 15, "character": 22 }, "end": { "line": 15, "character": 28 } }, "selectionRange": { "start": { "line": 15, "character": 22 }, "end": { "line": 15, "character": 28 } } }] }] ) ); client.shutdown(); } #[test] fn lsp_folding_range() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( 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 res = client.write_request( "textDocument/foldingRange", json!({ "textDocument": { "uri": "file:///a/file.ts" } }), ); assert_eq!( res, 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 }]) ); client.shutdown(); } #[test] fn lsp_rename() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( 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 res = client.write_request( "textDocument/rename", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 0, "character": 4 }, "newName": "variable_modified" }), ); assert_eq!( res, json!({ "documentChanges": [{ "textDocument": { "uri": "file:///a/file.ts", "version": 1 }, "edits": [{ "range": { "start": { "line": 0, "character": 4 }, "end": { "line": 0, "character": 12 } }, "newText": "variable_modified" }, { "range": { "start": { "line": 1, "character": 12 }, "end": { "line": 1, "character": 20 } }, "newText": "variable_modified" }] }] }) ); client.shutdown(); } #[test] fn lsp_selection_range() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( 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 res = client.write_request( "textDocument/selectionRange", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "positions": [{ "line": 2, "character": 8 }] }), ); assert_eq!( res, json!([{ "range": { "start": { "line": 2, "character": 8 }, "end": { "line": 2, "character": 9 } }, "parent": { "range": { "start": { "line": 2, "character": 8 }, "end": { "line": 2, "character": 15 } }, "parent": { "range": { "start": { "line": 2, "character": 4 }, "end": { "line": 4, "character": 5 } }, "parent": { "range": { "start": { "line": 1, "character": 13 }, "end": { "line": 6, "character": 2 } }, "parent": { "range": { "start": { "line": 1, "character": 12 }, "end": { "line": 6, "character": 3 } }, "parent": { "range": { "start": { "line": 1, "character": 2 }, "end": { "line": 6, "character": 3 } }, "parent": { "range": { "start": { "line": 0, "character": 11 }, "end": { "line": 7, "character": 0 } }, "parent": { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 7, "character": 1 } } } } } } } } } }]) ); client.shutdown(); } #[test] fn lsp_semantic_tokens() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "enum Values { value1, value2 }\n\nasync function baz(s: string): Promise {\n const r = s.slice(0);\n return r;\n}\n\ninterface IFoo {\n readonly x: number;\n foo(): boolean;\n}\n\nclass Bar implements IFoo {\n constructor(public readonly x: number) { }\n foo() { return true; }\n static staticBar = new Bar(0);\n private static getStaticBar() { return Bar.staticBar; }\n}\n" } }), ); let res = client.write_request( "textDocument/semanticTokens/full", json!({ "textDocument": { "uri": "file:///a/file.ts" } }), ); assert_eq!( res, 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 res = client.write_request( "textDocument/semanticTokens/range", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 6, "character": 0 } } }), ); assert_eq!( res, 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 ] }) ); client.shutdown(); } #[test] fn lsp_code_lens_references() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.change_configuration(json!({ "deno": { "enable": true, "codeLens": { "references": true, } }, })); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": concat!( "class A {\n", " a = \"a\";\n", "\n", " b() {\n", " console.log(this.a);\n", " }\n", "\n", " c() {\n", " this.a = \"c\";\n", " }\n", "}\n", "\n", "const a = new A();\n", "a.b();\n", "const b = 2;\n", "const c = 3;\n", "c; c;", ), } })); let res = client.write_request( "textDocument/codeLens", json!({ "textDocument": { "uri": "file:///a/file.ts" } }), ); assert_eq!( res, json!([{ "range": { "start": { "line": 0, "character": 6 }, "end": { "line": 0, "character": 7 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }, { "range": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 3 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }, { "range": { "start": { "line": 3, "character": 2 }, "end": { "line": 3, "character": 3 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }, { "range": { "start": { "line": 7, "character": 2 }, "end": { "line": 7, "character": 3 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }]) ); let res = 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" } }), ); assert_eq!( res, json!({ "range": { "start": { "line": 0, "character": 6 }, "end": { "line": 0, "character": 7 } }, "command": { "title": "1 reference", "command": "deno.client.showReferences", "arguments": [ "file:///a/file.ts", { "line": 0, "character": 6 }, [{ "uri": "file:///a/file.ts", "range": { "start": { "line": 12, "character": 14 }, "end": { "line": 12, "character": 15 } } }] ] } }) ); // 0 references let res = client.write_request( "codeLens/resolve", json!({ "range": { "start": { "line": 14, "character": 6 }, "end": { "line": 14, "character": 7 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }), ); assert_eq!( res, json!({ "range": { "start": { "line": 14, "character": 6 }, "end": { "line": 14, "character": 7 } }, "command": { "title": "0 references", "command": "", } }) ); // 2 references let res = client.write_request( "codeLens/resolve", json!({ "range": { "start": { "line": 15, "character": 6 }, "end": { "line": 15, "character": 7 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }), ); assert_eq!( res, json!({ "range": { "start": { "line": 15, "character": 6 }, "end": { "line": 15, "character": 7 } }, "command": { "title": "2 references", "command": "deno.client.showReferences", "arguments": [ "file:///a/file.ts", { "line": 15, "character": 6 }, [{ "uri": "file:///a/file.ts", "range": { "start": { "line": 16, "character": 0 }, "end": { "line": 16, "character": 1 } } },{ "uri": "file:///a/file.ts", "range": { "start": { "line": 16, "character": 3 }, "end": { "line": 16, "character": 4 } } }] ] } }) ); client.shutdown(); } #[test] fn lsp_code_lens_implementations() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.change_configuration(json!({ "deno": { "enable": true, "codeLens": { "implementations": true, "references": true, } }, })); client.did_open( 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 res = client.write_request( "textDocument/codeLens", json!({ "textDocument": { "uri": "file:///a/file.ts" } }), ); assert_eq!( res, json!([ { "range": { "start": { "line": 0, "character": 10 }, "end": { "line": 0, "character": 11 } }, "data": { "specifier": "file:///a/file.ts", "source": "implementations" } }, { "range": { "start": { "line": 0, "character": 10 }, "end": { "line": 0, "character": 11 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }, { "range": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 3 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }, { "range": { "start": { "line": 4, "character": 6 }, "end": { "line": 4, "character": 7 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }, { "range": { "start": { "line": 5, "character": 2 }, "end": { "line": 5, "character": 3 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }, { "range": { "start": { "line": 10, "character": 10 }, "end": { "line": 10, "character": 11 } }, "data": { "specifier": "file:///a/file.ts", "source": "implementations" } }, { "range": { "start": { "line": 10, "character": 10 }, "end": { "line": 10, "character": 11 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }, { "range": { "start": { "line": 11, "character": 2 }, "end": { "line": 11, "character": 3 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }]) ); let res = 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" } }), ); assert_eq!( res, json!({ "range": { "start": { "line": 0, "character": 10 }, "end": { "line": 0, "character": 11 } }, "command": { "title": "1 implementation", "command": "deno.client.showReferences", "arguments": [ "file:///a/file.ts", { "line": 0, "character": 10 }, [{ "uri": "file:///a/file.ts", "range": { "start": { "line": 4, "character": 6 }, "end": { "line": 4, "character": 7 } } }] ] } }) ); let res = client.write_request( "codeLens/resolve", json!({ "range": { "start": { "line": 10, "character": 10 }, "end": { "line": 10, "character": 11 } }, "data": { "specifier": "file:///a/file.ts", "source": "implementations" } }), ); assert_eq!( res, json!({ "range": { "start": { "line": 10, "character": 10 }, "end": { "line": 10, "character": 11 } }, "command": { "title": "0 implementations", "command": "" } }) ); client.shutdown(); } #[test] fn lsp_code_lens_test() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.disable_testing_api().set_code_lens(None); }); client.did_open( json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "const { test } = Deno;\nconst { test: test2 } = Deno;\nconst test3 = Deno.test;\n\nDeno.test(\"test a\", () => {});\nDeno.test({\n name: \"test b\",\n fn() {},\n});\ntest({\n name: \"test c\",\n fn() {},\n});\ntest(\"test d\", () => {});\ntest2({\n name: \"test e\",\n fn() {},\n});\ntest2(\"test f\", () => {});\ntest3({\n name: \"test g\",\n fn() {},\n});\ntest3(\"test h\", () => {});\n" } }), ); let res = client.write_request( "textDocument/codeLens", json!({ "textDocument": { "uri": "file:///a/file.ts" } }), ); assert_eq!( res, json!([{ "range": { "start": { "line": 4, "character": 5 }, "end": { "line": 4, "character": 9 } }, "command": { "title": "▶︎ Run Test", "command": "deno.client.test", "arguments": [ "file:///a/file.ts", "test a", { "inspect": false } ] } }, { "range": { "start": { "line": 4, "character": 5 }, "end": { "line": 4, "character": 9 } }, "command": { "title": "Debug", "command": "deno.client.test", "arguments": [ "file:///a/file.ts", "test a", { "inspect": true } ] } }, { "range": { "start": { "line": 5, "character": 5 }, "end": { "line": 5, "character": 9 } }, "command": { "title": "▶︎ Run Test", "command": "deno.client.test", "arguments": [ "file:///a/file.ts", "test b", { "inspect": false } ] } }, { "range": { "start": { "line": 5, "character": 5 }, "end": { "line": 5, "character": 9 } }, "command": { "title": "Debug", "command": "deno.client.test", "arguments": [ "file:///a/file.ts", "test b", { "inspect": true } ] } }, { "range": { "start": { "line": 9, "character": 0 }, "end": { "line": 9, "character": 4 } }, "command": { "title": "▶︎ Run Test", "command": "deno.client.test", "arguments": [ "file:///a/file.ts", "test c", { "inspect": false } ] } }, { "range": { "start": { "line": 9, "character": 0 }, "end": { "line": 9, "character": 4 } }, "command": { "title": "Debug", "command": "deno.client.test", "arguments": [ "file:///a/file.ts", "test c", { "inspect": true } ] } }, { "range": { "start": { "line": 13, "character": 0 }, "end": { "line": 13, "character": 4 } }, "command": { "title": "▶︎ Run Test", "command": "deno.client.test", "arguments": [ "file:///a/file.ts", "test d", { "inspect": false } ] } }, { "range": { "start": { "line": 13, "character": 0 }, "end": { "line": 13, "character": 4 } }, "command": { "title": "Debug", "command": "deno.client.test", "arguments": [ "file:///a/file.ts", "test d", { "inspect": true } ] } }, { "range": { "start": { "line": 14, "character": 0 }, "end": { "line": 14, "character": 5 } }, "command": { "title": "▶︎ Run Test", "command": "deno.client.test", "arguments": [ "file:///a/file.ts", "test e", { "inspect": false } ] } }, { "range": { "start": { "line": 14, "character": 0 }, "end": { "line": 14, "character": 5 } }, "command": { "title": "Debug", "command": "deno.client.test", "arguments": [ "file:///a/file.ts", "test e", { "inspect": true } ] } }, { "range": { "start": { "line": 18, "character": 0 }, "end": { "line": 18, "character": 5 } }, "command": { "title": "▶︎ Run Test", "command": "deno.client.test", "arguments": [ "file:///a/file.ts", "test f", { "inspect": false } ] } }, { "range": { "start": { "line": 18, "character": 0 }, "end": { "line": 18, "character": 5 } }, "command": { "title": "Debug", "command": "deno.client.test", "arguments": [ "file:///a/file.ts", "test f", { "inspect": true } ] } }, { "range": { "start": { "line": 19, "character": 0 }, "end": { "line": 19, "character": 5 } }, "command": { "title": "▶︎ Run Test", "command": "deno.client.test", "arguments": [ "file:///a/file.ts", "test g", { "inspect": false } ] } }, { "range": { "start": { "line": 19, "character": 0 }, "end": { "line": 19, "character": 5 } }, "command": { "title": "Debug", "command": "deno.client.test", "arguments": [ "file:///a/file.ts", "test g", { "inspect": true } ] } }, { "range": { "start": { "line": 23, "character": 0 }, "end": { "line": 23, "character": 5 } }, "command": { "title": "▶︎ Run Test", "command": "deno.client.test", "arguments": [ "file:///a/file.ts", "test h", { "inspect": false } ] } }, { "range": { "start": { "line": 23, "character": 0 }, "end": { "line": 23, "character": 5 } }, "command": { "title": "Debug", "command": "deno.client.test", "arguments": [ "file:///a/file.ts", "test h", { "inspect": true } ] } }]) ); client.shutdown(); } #[test] fn lsp_code_lens_test_disabled() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.disable_testing_api().set_code_lens(Some(json!({ "implementations": true, "references": true, "test": false }))); }); client.change_configuration(json!({ "deno": { "enable": true, "codeLens": { "test": false, }, }, })); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "const { test } = Deno;\nconst { test: test2 } = Deno;\nconst test3 = Deno.test;\n\nDeno.test(\"test a\", () => {});\nDeno.test({\n name: \"test b\",\n fn() {},\n});\ntest({\n name: \"test c\",\n fn() {},\n});\ntest(\"test d\", () => {});\ntest2({\n name: \"test e\",\n fn() {},\n});\ntest2(\"test f\", () => {});\ntest3({\n name: \"test g\",\n fn() {},\n});\ntest3(\"test h\", () => {});\n" }, })); let res = client.write_request( "textDocument/codeLens", json!({ "textDocument": { "uri": "file:///a/file.ts" } }), ); assert_eq!(res, json!(null)); client.shutdown(); } #[test] fn lsp_code_lens_non_doc_nav_tree() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.change_configuration(json!({ "deno": { "enable": true, "codeLens": { "implementations": true, "references": true, } }, })); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "console.log(Date.now());\n" } })); client.write_request( "textDocument/references", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 0, "character": 3 }, "context": { "includeDeclaration": true } }), ); client.write_request( "deno/virtualTextDocument", json!({ "textDocument": { "uri": "deno:/asset/lib.deno.shared_globals.d.ts" } }), ); let res = client.write_request_with_res_as::>( "textDocument/codeLens", json!({ "textDocument": { "uri": "deno:/asset/lib.deno.shared_globals.d.ts" } }), ); assert!(res.len() > 50); client.write_request_with_res_as::( "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" } }), ); client.shutdown(); } #[test] fn lsp_nav_tree_updates() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.change_configuration(json!({ "deno": { "enable": true, "codeLens": { "implementations": true, "references": true, } }, })); client.did_open( 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 res = client.write_request( "textDocument/codeLens", json!({ "textDocument": { "uri": "file:///a/file.ts" } }), ); assert_eq!( res, json!([ { "range": { "start": { "line": 0, "character": 10 }, "end": { "line": 0, "character": 11 } }, "data": { "specifier": "file:///a/file.ts", "source": "implementations" } }, { "range": { "start": { "line": 0, "character": 10 }, "end": { "line": 0, "character": 11 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }, { "range": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 3 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }, { "range": { "start": { "line": 4, "character": 6 }, "end": { "line": 4, "character": 7 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }, { "range": { "start": { "line": 5, "character": 2 }, "end": { "line": 5, "character": 3 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }, { "range": { "start": { "line": 10, "character": 10 }, "end": { "line": 10, "character": 11 } }, "data": { "specifier": "file:///a/file.ts", "source": "implementations" } }, { "range": { "start": { "line": 10, "character": 10 }, "end": { "line": 10, "character": 11 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }, { "range": { "start": { "line": 11, "character": 2 }, "end": { "line": 11, "character": 3 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }]) ); 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": "" } ] }), ); let res = client.write_request( "textDocument/codeLens", json!({ "textDocument": { "uri": "file:///a/file.ts" } }), ); assert_eq!( res, json!([{ "range": { "start": { "line": 0, "character": 10 }, "end": { "line": 0, "character": 11 } }, "data": { "specifier": "file:///a/file.ts", "source": "implementations" } }, { "range": { "start": { "line": 0, "character": 10 }, "end": { "line": 0, "character": 11 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }, { "range": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 3 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }, { "range": { "start": { "line": 4, "character": 6 }, "end": { "line": 4, "character": 7 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }, { "range": { "start": { "line": 5, "character": 2 }, "end": { "line": 5, "character": 3 } }, "data": { "specifier": "file:///a/file.ts", "source": "references" } }]) ); client.shutdown(); } #[test] fn lsp_find_references() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/mod.ts", "languageId": "typescript", "version": 1, "text": r"export const a = 1;\nconst b = 2;" } })); client.did_open(json!({ "textDocument": { "uri": "file:///a/mod.test.ts", "languageId": "typescript", "version": 1, "text": r#"import { a } from './mod.ts'; console.log(a);"# } })); // test without including the declaration let res = client.write_request( "textDocument/references", json!({ "textDocument": { "uri": "file:///a/mod.ts", }, "position": { "line": 0, "character": 13 }, "context": { "includeDeclaration": false } }), ); assert_eq!( res, json!([{ "uri": "file:///a/mod.test.ts", "range": { "start": { "line": 0, "character": 9 }, "end": { "line": 0, "character": 10 } } }, { "uri": "file:///a/mod.test.ts", "range": { "start": { "line": 0, "character": 42 }, "end": { "line": 0, "character": 43 } } }]) ); // test with including the declaration let res = client.write_request( "textDocument/references", json!({ "textDocument": { "uri": "file:///a/mod.ts", }, "position": { "line": 0, "character": 13 }, "context": { "includeDeclaration": true } }), ); assert_eq!( res, json!([{ "uri": "file:///a/mod.ts", "range": { "start": { "line": 0, "character": 13 }, "end": { "line": 0, "character": 14 } } }, { "uri": "file:///a/mod.test.ts", "range": { "start": { "line": 0, "character": 9 }, "end": { "line": 0, "character": 10 } } }, { "uri": "file:///a/mod.test.ts", "range": { "start": { "line": 0, "character": 42 }, "end": { "line": 0, "character": 43 } } }]) ); // test 0 references let res = client.write_request( "textDocument/references", json!({ "textDocument": { "uri": "file:///a/mod.ts", }, "position": { "line": 1, "character": 6 }, "context": { "includeDeclaration": false } }), ); assert_eq!(res, json!(null)); // seems it always returns null for this, which is ok client.shutdown(); } #[test] fn lsp_signature_help() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( 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 res = client.write_request( "textDocument/signatureHelp", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "character": 4, "line": 9 }, "context": { "triggerKind": 2, "triggerCharacter": "(", "isRetrigger": false } }), ); assert_eq!( res, 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, " } ] }), ); let res = client.write_request( "textDocument/signatureHelp", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "character": 8, "line": 9 } }), ); assert_eq!( res, 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 }) ); client.shutdown(); } #[test] fn lsp_code_actions() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( 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 res = client .write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "range": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 7 } }, "context": { "diagnostics": [{ "range": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 7 } }, "severity": 1, "code": 1308, "source": "deno-ts", "message": "'await' expressions are only allowed within async functions and at the top levels of modules.", "relatedInformation": [] }], "only": ["quickfix"] } }), ) ; assert_eq!( res, json!([{ "title": "Add async modifier to containing function", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 7 } }, "severity": 1, "code": 1308, "source": "deno-ts", "message": "'await' expressions are only allowed within async functions and at the top levels of modules.", "relatedInformation": [] }], "edit": { "documentChanges": [{ "textDocument": { "uri": "file:///a/file.ts", "version": 1 }, "edits": [{ "range": { "start": { "line": 0, "character": 7 }, "end": { "line": 0, "character": 7 } }, "newText": "async " }, { "range": { "start": { "line": 0, "character": 21 }, "end": { "line": 0, "character": 25 } }, "newText": "Promise" }] }] } }, { "title": "Add all missing 'async' modifiers", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 7 } }, "severity": 1, "code": 1308, "source": "deno-ts", "message": "'await' expressions are only allowed within async functions and at the top levels of modules.", "relatedInformation": [] }], "data": { "specifier": "file:///a/file.ts", "fixId": "fixAwaitInSyncFunction" } }]) ); let res = client .write_request( "codeAction/resolve", json!({ "title": "Add all missing 'async' modifiers", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 7 } }, "severity": 1, "code": 1308, "source": "deno-ts", "message": "'await' expressions are only allowed within async functions and at the top levels of modules.", "relatedInformation": [] }], "data": { "specifier": "file:///a/file.ts", "fixId": "fixAwaitInSyncFunction" } }), ); assert_eq!( res, json!({ "title": "Add all missing 'async' modifiers", "kind": "quickfix", "diagnostics": [ { "range": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 7 } }, "severity": 1, "code": 1308, "source": "deno-ts", "message": "'await' expressions are only allowed within async functions and at the top levels of modules.", "relatedInformation": [] } ], "edit": { "documentChanges": [{ "textDocument": { "uri": "file:///a/file.ts", "version": 1 }, "edits": [{ "range": { "start": { "line": 0, "character": 7 }, "end": { "line": 0, "character": 7 } }, "newText": "async " }, { "range": { "start": { "line": 0, "character": 21 }, "end": { "line": 0, "character": 25 } }, "newText": "Promise" }, { "range": { "start": { "line": 4, "character": 7 }, "end": { "line": 4, "character": 7 } }, "newText": "async " }, { "range": { "start": { "line": 4, "character": 21 }, "end": { "line": 4, "character": 25 } }, "newText": "Promise" }] }] }, "data": { "specifier": "file:///a/file.ts", "fixId": "fixAwaitInSyncFunction" } }) ); client.shutdown(); } #[test] fn test_lsp_code_actions_ordering() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": r#" import "https://deno.land/x/a/mod.ts"; let a = "a"; console.log(a); export function b(): void { await Promise.resolve("b"); } "# } })); let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "range": { "start": { "line": 1, "character": 11 }, "end": { "line": 6, "character": 12 } }, "context": { "diagnostics": diagnostics.all(), "only": ["quickfix"] } }), ); // Simplify the serialization to `{ title, source }` for this test. let mut actions: Vec = serde_json::from_value(res).unwrap(); for action in &mut actions { let action = action.as_object_mut().unwrap(); let title = action.get("title").unwrap().as_str().unwrap().to_string(); let diagnostics = action.get("diagnostics").unwrap().as_array().unwrap(); let diagnostic = diagnostics.first().unwrap().as_object().unwrap(); let source = diagnostic.get("source").unwrap(); let source = source.as_str().unwrap().to_string(); action.clear(); action.insert("title".to_string(), serde_json::to_value(title).unwrap()); action.insert("source".to_string(), serde_json::to_value(source).unwrap()); } let res = serde_json::to_value(actions).unwrap(); // Ensure ordering is "deno" -> "deno-ts" -> "deno-lint". assert_eq!( res, json!([ { "title": "Cache \"https://deno.land/x/a/mod.ts\" and its dependencies.", "source": "deno", }, { "title": "Add async modifier to containing function", "source": "deno-ts", }, { "title": "Disable prefer-const for this line", "source": "deno-lint", }, { "title": "Disable prefer-const for the entire file", "source": "deno-lint", }, { "title": "Ignore lint errors for the entire file", "source": "deno-lint", }, { "title": "Disable no-await-in-sync-fn for this line", "source": "deno-lint", }, { "title": "Disable no-await-in-sync-fn for the entire file", "source": "deno-lint", }, { "title": "Ignore lint errors for the entire file", "source": "deno-lint", }, ]) ); client.shutdown(); } #[test] fn lsp_status_file() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let res = client.write_request( "deno/virtualTextDocument", json!({ "textDocument": { "uri": "deno:/status.md" } }), ); let res = res.as_str().unwrap().to_string(); assert!(res.starts_with("# Deno Language Server Status")); let res = client.write_request( "deno/virtualTextDocument", json!({ "textDocument": { "uri": "deno:/status.md?1" } }), ); let res = res.as_str().unwrap().to_string(); assert!(res.starts_with("# Deno Language Server Status")); client.shutdown(); } #[test] fn lsp_code_actions_deno_cache() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.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.messages_with_source("deno"), serde_json::from_value(json!({ "uri": "file:///a/file.ts", "diagnostics": [{ "range": { "start": { "line": 0, "character": 19 }, "end": { "line": 0, "character": 49 } }, "severity": 1, "code": "no-cache", "source": "deno", "message": "Uncached or missing remote URL: https://deno.land/x/a/mod.ts", "data": { "specifier": "https://deno.land/x/a/mod.ts" } }], "version": 1 })).unwrap() ); let res = client .write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "range": { "start": { "line": 0, "character": 19 }, "end": { "line": 0, "character": 49 } }, "context": { "diagnostics": [{ "range": { "start": { "line": 0, "character": 19 }, "end": { "line": 0, "character": 49 } }, "severity": 1, "code": "no-cache", "source": "deno", "message": "Unable to load the remote module: \"https://deno.land/x/a/mod.ts\".", "data": { "specifier": "https://deno.land/x/a/mod.ts" } }], "only": ["quickfix"] } }), ); assert_eq!( res, json!([{ "title": "Cache \"https://deno.land/x/a/mod.ts\" and its dependencies.", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 0, "character": 19 }, "end": { "line": 0, "character": 49 } }, "severity": 1, "code": "no-cache", "source": "deno", "message": "Unable to load the remote module: \"https://deno.land/x/a/mod.ts\".", "data": { "specifier": "https://deno.land/x/a/mod.ts" } }], "command": { "title": "", "command": "deno.cache", "arguments": [["https://deno.land/x/a/mod.ts"], "file:///a/file.ts"] } }]) ); client.shutdown(); } #[test] fn lsp_code_actions_deno_cache_jsr() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#" import { add } from "jsr:@denotest/add@1"; console.log(add(1, 2)); "#, }, })); assert_eq!( json!(diagnostics.messages_with_source("deno")), json!({ "uri": temp_dir.url().join("file.ts").unwrap(), "diagnostics": [{ "range": { "start": { "line": 1, "character": 28 }, "end": { "line": 1, "character": 49 }, }, "severity": 1, "code": "not-installed-jsr", "source": "deno", "message": "JSR package \"@denotest/add@1\" is not installed or doesn't exist.", "data": { "specifier": "jsr:@denotest/add@1" }, }], "version": 1, }) ); let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap() }, "range": { "start": { "line": 1, "character": 28 }, "end": { "line": 1, "character": 49 }, }, "context": { "diagnostics": [{ "range": { "start": { "line": 1, "character": 28 }, "end": { "line": 1, "character": 49 }, }, "severity": 1, "code": "not-installed-jsr", "source": "deno", "message": "JSR package \"@denotest/add@1\" is not installed or doesn't exist.", "data": { "specifier": "jsr:@denotest/add@1" }, }], "only": ["quickfix"], } }), ); assert_eq!( res, json!([{ "title": "Install \"jsr:@denotest/add@1\" and its dependencies.", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 1, "character": 28 }, "end": { "line": 1, "character": 49 }, }, "severity": 1, "code": "not-installed-jsr", "source": "deno", "message": "JSR package \"@denotest/add@1\" is not installed or doesn't exist.", "data": { "specifier": "jsr:@denotest/add@1" }, }], "command": { "title": "", "command": "deno.cache", "arguments": [ ["jsr:@denotest/add@1"], temp_dir.url().join("file.ts").unwrap(), ], }, }]) ); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [ ["jsr:@denotest/add@1"], temp_dir.url().join("file.ts").unwrap(), ], }), ); let diagnostics = client.read_diagnostics(); assert_eq!(json!(diagnostics.all()), json!([])); client.shutdown(); } #[test] fn lsp_jsr_lockfile() { let context = TestContextBuilder::for_jsr().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write("./deno.json", json!({}).to_string()); let lockfile = temp_dir.path().join("deno.lock"); let integrity = context.get_jsr_package_integrity("@denotest/add/0.2.0"); lockfile.write_json(&json!({ "version": "3", "packages": { "specifiers": { // This is an old version of the package which exports `sum()` instead // of `add()`. "jsr:@denotest/add": "jsr:@denotest/add@0.2.0", }, "jsr": { "@denotest/add@0.2.0": { "integrity": integrity } } }, "remote": {}, })); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#" import { sum } from "jsr:@denotest/add"; console.log(sum(1, 2)); "#, }, })); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [ [], temp_dir.url().join("file.ts").unwrap(), ], }), ); let diagnostics = client.read_diagnostics(); assert_eq!(json!(diagnostics.all()), json!([])); client.shutdown(); } #[test] fn lsp_jsr_auto_import_completion() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.write( "main.ts", r#" import "jsr:@denotest/add@1"; "#, ); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [ [], temp_dir.url().join("main.ts").unwrap(), ], }), ); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#"add"#, } })); let list = client.get_completion_list( temp_dir.url().join("file.ts").unwrap(), (0, 3), json!({ "triggerKind": 1 }), ); assert!(!list.is_incomplete); assert_eq!(list.items.len(), 268); let item = list.items.iter().find(|i| i.label == "add").unwrap(); assert_eq!(&item.label, "add"); assert_eq!( json!(&item.label_details), json!({ "description": "jsr:@denotest/add@1" }) ); let res = client.write_request("completionItem/resolve", json!(item)); assert_eq!( res, json!({ "label": "add", "labelDetails": { "description": "jsr:@denotest/add@1" }, "kind": 3, "detail": "function add(a: number, b: number): number", "documentation": { "kind": "markdown", "value": "" }, "sortText": "\u{ffff}16_1", "additionalTextEdits": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, }, "newText": "import { add } from \"jsr:@denotest/add@1\";\n\n", }, ], }) ); client.shutdown(); } #[test] fn lsp_jsr_auto_import_completion_import_map() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.json", json!({ "imports": { "add": "jsr:@denotest/add@^1.0", }, }) .to_string(), ); temp_dir.write( "main.ts", r#" import "jsr:@denotest/add@1"; "#, ); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [ [], temp_dir.url().join("main.ts").unwrap(), ], }), ); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#"add"#, } })); let list = client.get_completion_list( temp_dir.url().join("file.ts").unwrap(), (0, 3), json!({ "triggerKind": 1 }), ); assert!(!list.is_incomplete); assert_eq!(list.items.len(), 268); let item = list.items.iter().find(|i| i.label == "add").unwrap(); assert_eq!(&item.label, "add"); assert_eq!(json!(&item.label_details), json!({ "description": "add" })); let res = client.write_request("completionItem/resolve", json!(item)); assert_eq!( res, json!({ "label": "add", "labelDetails": { "description": "add" }, "kind": 3, "detail": "function add(a: number, b: number): number", "documentation": { "kind": "markdown", "value": "" }, "sortText": "\u{ffff}16_0", "additionalTextEdits": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, }, "newText": "import { add } from \"add\";\n\n", }, ], }) ); client.shutdown(); } #[test] fn lsp_jsr_auto_import_completion_import_map_sub_path() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.json", json!({ "imports": { "@std/path": "jsr:@std/path@^0.220.1", }, }) .to_string(), ); let file = source_file( temp_dir.path().join("file.ts"), r#" // Adds jsr:@std/path@^0.220.1/normalize to the module graph. import "jsr:@std/url@^0.220.1/normalize"; normalize "#, ); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], file.url()], }), ); client.read_diagnostics(); client.did_open_file(&file); let list = client.get_completion_list( file.url(), (3, 15), json!({ "triggerKind": 1 }), ); let item = list .items .iter() .find(|i| { i.label == "normalize" && json!(&i.label_details) .to_string() .contains("\"@std/path/posix/normalize\"") }) .unwrap(); let res = client.write_request("completionItem/resolve", json!(item)); assert_eq!( res, json!({ "label": "normalize", "labelDetails": { "description": "@std/path/posix/normalize" }, "kind": 3, "detail": "function normalize(path: string): string", "documentation": { "kind": "markdown", "value": "Normalize the `path`, resolving `'..'` and `'.'` segments.\nNote that resolving these segments does not necessarily mean that all will be eliminated.\nA `'..'` at the top-level will be preserved, and an empty path is canonically `'.'`.\n\n*@param* - path to be normalized" }, "sortText": "\u{ffff}16_0", "additionalTextEdits": [ { "range": { "start": { "line": 2, "character": 6 }, "end": { "line": 2, "character": 6 }, }, "newText": "import { normalize } from \"@std/path/posix/normalize\";\n", }, ], }), ); client.shutdown(); } #[test] fn lsp_jsr_code_action_missing_declaration() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); let file = source_file( temp_dir.path().join("file.ts"), r#" import { someFunction } from "jsr:@denotest/types-file"; assertReturnType(someFunction()); "#, ); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], file.url()], }), ); client.did_open_file(&file); let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": file.url(), }, "range": { "start": { "line": 2, "character": 6 }, "end": { "line": 2, "character": 22 }, }, "context": { "diagnostics": [ { "range": { "start": { "line": 2, "character": 6 }, "end": { "line": 2, "character": 22 }, }, "severity": 8, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'assertReturnType'.", "relatedInformation": [], }, ], "only": ["quickfix"], }, }), ); assert_eq!( res, json!([ { "title": "Add missing function declaration 'assertReturnType'", "kind": "quickfix", "diagnostics": [ { "range": { "start": { "line": 2, "character": 6, }, "end": { "line": 2, "character": 22, }, }, "severity": 8, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'assertReturnType'.", "relatedInformation": [], }, ], "edit": { "documentChanges": [ { "textDocument": { "uri": file.url(), "version": 1, }, "edits": [ { "range": { "start": { "line": 1, "character": 6, }, "end": { "line": 1, "character": 6, }, }, "newText": "import { ReturnType } from \"jsr:@denotest/types-file/types\";\n", }, { "range": { "start": { "line": 3, "character": 0, }, "end": { "line": 3, "character": 0, }, }, "newText": "\n function assertReturnType(arg0: ReturnType) {\n throw new Error(\"Function not implemented.\");\n }\n", }, ], }, ], }, }, ]) ); } #[test] fn lsp_code_actions_deno_cache_npm() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.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.messages_with_source("deno"), serde_json::from_value(json!({ "uri": "file:///a/file.ts", "diagnostics": [{ "range": { "start": { "line": 0, "character": 18 }, "end": { "line": 0, "character": 29 } }, "severity": 1, "code": "not-installed-npm", "source": "deno", "message": "NPM package \"chalk\" is not installed or doesn't exist.", "data": { "specifier": "npm:chalk" } }], "version": 1 })) .unwrap() ); let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "range": { "start": { "line": 0, "character": 18 }, "end": { "line": 0, "character": 29 } }, "context": { "diagnostics": [{ "range": { "start": { "line": 0, "character": 18 }, "end": { "line": 0, "character": 29 } }, "severity": 1, "code": "not-installed-npm", "source": "deno", "message": "NPM package \"chalk\" is not installed or doesn't exist.", "data": { "specifier": "npm:chalk" } }], "only": ["quickfix"] } }), ); assert_eq!( res, json!([{ "title": "Install \"npm:chalk\" and its dependencies.", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 0, "character": 18 }, "end": { "line": 0, "character": 29 } }, "severity": 1, "code": "not-installed-npm", "source": "deno", "message": "NPM package \"chalk\" is not installed or doesn't exist.", "data": { "specifier": "npm:chalk" } }], "command": { "title": "", "command": "deno.cache", "arguments": [["npm:chalk"], "file:///a/file.ts"] } }]) ); client.shutdown(); } #[test] fn lsp_code_actions_deno_cache_all() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": r#" import * as a from "https://deno.land/x/a/mod.ts"; import chalk from "npm:chalk"; console.log(a); console.log(chalk); "#, } })); assert_eq!( diagnostics.messages_with_source("deno"), serde_json::from_value(json!({ "uri": "file:///a/file.ts", "diagnostics": [ { "range": { "start": { "line": 1, "character": 27 }, "end": { "line": 1, "character": 57 }, }, "severity": 1, "code": "no-cache", "source": "deno", "message": "Uncached or missing remote URL: https://deno.land/x/a/mod.ts", "data": { "specifier": "https://deno.land/x/a/mod.ts" }, }, { "range": { "start": { "line": 2, "character": 26 }, "end": { "line": 2, "character": 37 }, }, "severity": 1, "code": "not-installed-npm", "source": "deno", "message": "NPM package \"chalk\" is not installed or doesn't exist.", "data": { "specifier": "npm:chalk" }, }, ], "version": 1, })).unwrap() ); let res = client .write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "range": { "start": { "line": 1, "character": 27 }, "end": { "line": 1, "character": 57 }, }, "context": { "diagnostics": [{ "range": { "start": { "line": 1, "character": 27 }, "end": { "line": 1, "character": 57 }, }, "severity": 1, "code": "no-cache", "source": "deno", "message": "Uncached or missing remote URL: https://deno.land/x/a/mod.ts", "data": { "specifier": "https://deno.land/x/a/mod.ts", }, }], "only": ["quickfix"], } }), ); assert_eq!( res, json!([ { "title": "Cache \"https://deno.land/x/a/mod.ts\" and its dependencies.", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 1, "character": 27 }, "end": { "line": 1, "character": 57 }, }, "severity": 1, "code": "no-cache", "source": "deno", "message": "Uncached or missing remote URL: https://deno.land/x/a/mod.ts", "data": { "specifier": "https://deno.land/x/a/mod.ts", }, }], "command": { "title": "", "command": "deno.cache", "arguments": [["https://deno.land/x/a/mod.ts"], "file:///a/file.ts"], } }, { "title": "Cache all dependencies of this module.", "kind": "quickfix", "diagnostics": [ { "range": { "start": { "line": 1, "character": 27 }, "end": { "line": 1, "character": 57 }, }, "severity": 1, "code": "no-cache", "source": "deno", "message": "Uncached or missing remote URL: https://deno.land/x/a/mod.ts", "data": { "specifier": "https://deno.land/x/a/mod.ts", }, }, { "range": { "start": { "line": 2, "character": 26 }, "end": { "line": 2, "character": 37 }, }, "severity": 1, "code": "not-installed-npm", "source": "deno", "message": "NPM package \"chalk\" is not installed or doesn't exist.", "data": { "specifier": "npm:chalk" }, }, ], "command": { "title": "", "command": "deno.cache", "arguments": [[], "file:///a/file.ts"], } }, ]) ); client.shutdown(); } #[test] fn lsp_code_actions_deno_types_for_npm() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .add_npm_env_vars() .build(); let temp_dir = context.temp_dir(); temp_dir.write("deno.json", json!({}).to_string()); temp_dir.write( "package.json", json!({ "dependencies": { "react": "^18.2.0", "@types/react": "^18.3.10", }, }) .to_string(), ); temp_dir.create_dir_all("managed_node_modules"); temp_dir.write( "managed_node_modules/deno.json", json!({ "nodeModulesDir": false, }) .to_string(), ); context.run_npm("install"); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import \"react\";\n", } })); let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), }, "range": { "start": { "line": 0, "character": 7 }, "end": { "line": 0, "character": 7 }, }, "context": { "diagnostics": [], "only": ["quickfix"] }, }), ); assert_eq!( res, json!([ { "title": "Add @deno-types directive for \"@types/react\"", "kind": "quickfix", "edit": { "changes": { temp_dir.url().join("file.ts").unwrap(): [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, }, "newText": "// @deno-types=\"@types/react\"\n", }, ], }, }, }, ]), ); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("managed_node_modules/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import \"npm:react\";\n", } })); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [ [], temp_dir.url().join("managed_node_modules/file.ts").unwrap(), ], }), ); let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": temp_dir.url().join("managed_node_modules/file.ts").unwrap(), }, "range": { "start": { "line": 0, "character": 7 }, "end": { "line": 0, "character": 7 }, }, "context": { "diagnostics": [], "only": ["quickfix"] }, }), ); assert_eq!( res, json!([ { "title": "Add @deno-types directive for \"npm:@types/react@^18.3.10\"", "kind": "quickfix", "edit": { "changes": { temp_dir.url().join("managed_node_modules/file.ts").unwrap(): [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, }, "newText": "// @deno-types=\"npm:@types/react@^18.3.10\"\n", }, ], }, }, }, ]), ); client.shutdown(); } #[test] fn lsp_cache_on_save() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.write( "file.ts", r#" import { printHello } from "http://localhost:4545/subdir/print_hello.ts"; printHello(); "#, ); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.change_configuration(json!({ "deno": { "enable": true, "cacheOnSave": true, }, })); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": temp_dir.read_to_string("file.ts"), } })); assert_eq!( diagnostics.messages_with_source("deno"), serde_json::from_value(json!({ "uri": temp_dir.url().join("file.ts").unwrap(), "diagnostics": [{ "range": { "start": { "line": 1, "character": 33 }, "end": { "line": 1, "character": 78 } }, "severity": 1, "code": "no-cache", "source": "deno", "message": "Uncached or missing remote URL: http://localhost:4545/subdir/print_hello.ts", "data": { "specifier": "http://localhost:4545/subdir/print_hello.ts" } }], "version": 1 })) .unwrap() ); client.did_save(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap() }, })); assert_eq!(client.read_diagnostics().all(), vec![]); client.shutdown(); } // Regression test for https://github.com/denoland/deno/issues/25999. #[test] fn lsp_asset_document_dom_code_action() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.json", json!({ "compilerOptions": { "lib": ["deno.window", "dom"], }, }) .to_string(), ); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#""#, }, })); let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": "asset:///lib.dom.d.ts" }, "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, }, "context": { "diagnostics": [], "only": ["quickfix"] }, }), ); assert_eq!(res, json!(null)); client.shutdown(); } // Regression test for https://github.com/denoland/deno/issues/22122. #[test] fn lsp_cache_then_definition() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#"import "http://localhost:4545/run/002_hello.ts";"#, }, })); // Prior to the fix, this would cause a faulty memoization that maps the // URL "http://localhost:4545/run/002_hello.ts" to itself, preventing it from // being reverse-mapped to "deno:/http/localhost%3A4545/run/002_hello.ts" on // "textDocument/definition" request. client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [ ["http://localhost:4545/run/002_hello.ts"], temp_dir.url().join("file.ts").unwrap(), ], }), ); let res = client.write_request( "textDocument/definition", json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap() }, "position": { "line": 0, "character": 8 }, }), ); assert_eq!( res, json!([{ "targetUri": "deno:/http/localhost%3A4545/run/002_hello.ts", "targetRange": { "start": { "line": 0, "character": 0, }, "end": { "line": 1, "character": 0, }, }, "targetSelectionRange": { "start": { "line": 0, "character": 0, }, "end": { "line": 1, "character": 0, }, }, }]), ); client.shutdown(); } #[test] fn lsp_code_actions_imports() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.change_configuration(json!({ "deno": { "enable": true, }, "typescript": { "preferences": { "preferTypeOnlyAutoImports": true, }, }, })); client.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); } } "# } })); client.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; } } "# } })); client.did_open(json!({ "textDocument": { "uri": "file:///a/file02.ts", "languageId": "typescript", "version": 1, "text": r#"export interface DuckConfigOptions { kind: string; quacks: boolean; } "# } })); let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": "file:///a/file00.ts" }, "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 6, "character": 0 } }, "context": { "diagnostics": [{ "range": { "start": { "line": 0, "character": 50 }, "end": { "line": 0, "character": 67 } }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'DuckConfigOptions'." }, { "range": { "start": { "line": 4, "character": 39 }, "end": { "line": 4, "character": 49 } }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'DuckConfig'." }], "only": ["quickfix"] } }), ); assert_eq!( res, json!([{ "title": "Add import from \"./file02.ts\"", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 0, "character": 50 }, "end": { "line": 0, "character": 67 } }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'DuckConfigOptions'." }], "edit": { "documentChanges": [{ "textDocument": { "uri": "file:///a/file00.ts", "version": 1 }, "edits": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "import type { DuckConfigOptions } from \"./file02.ts\";\n\n" }] }] } }, { "title": "Add all missing imports", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 0, "character": 50 }, "end": { "line": 0, "character": 67 } }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'DuckConfigOptions'." }], "data": { "specifier": "file:///a/file00.ts", "fixId": "fixMissingImport" } }, { "title": "Add import from \"./file01.ts\"", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 4, "character": 39 }, "end": { "line": 4, "character": 49 } }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'DuckConfig'." }], "edit": { "documentChanges": [{ "textDocument": { "uri": "file:///a/file00.ts", "version": 1 }, "edits": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "import { DuckConfig } from \"./file01.ts\";\n\n" }] }] } }]) ); let res = client.write_request( "codeAction/resolve", json!({ "title": "Add all missing imports", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 0, "character": 50 }, "end": { "line": 0, "character": 67 } }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'DuckConfigOptions'." }, { "range": { "start": { "line": 4, "character": 39 }, "end": { "line": 4, "character": 49 } }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'DuckConfig'." }], "data": { "specifier": "file:///a/file00.ts", "fixId": "fixMissingImport" } }), ); assert_eq!( res, json!({ "title": "Add all missing imports", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 0, "character": 50 }, "end": { "line": 0, "character": 67 } }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'DuckConfigOptions'." }, { "range": { "start": { "line": 4, "character": 39 }, "end": { "line": 4, "character": 49 } }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'DuckConfig'." }], "edit": { "documentChanges": [{ "textDocument": { "uri": "file:///a/file00.ts", "version": 1 }, "edits": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "import { DuckConfig } from \"./file01.ts\";\nimport type { DuckConfigOptions } from \"./file02.ts\";\n\n" }] }] }, "data": { "specifier": "file:///a/file00.ts", "fixId": "fixMissingImport" } }) ); client.shutdown(); } #[test] fn lsp_code_actions_imports_dts() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); source_file( temp_dir.path().join("decl.d.ts"), "export type SomeType = 1;\n", ); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#" const a: SomeType = 1; console.log(a); "#, } })); let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), }, "range": { "start": { "line": 1, "character": 17 }, "end": { "line": 1, "character": 25 }, }, "context": { "diagnostics": diagnostics.all(), "only": ["quickfix"], }, }), ); assert_eq!( res, json!([{ "title": "Add import from \"./decl.d.ts\"", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 1, "character": 17 }, "end": { "line": 1, "character": 25 }, }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'SomeType'.", }], "edit": { "documentChanges": [{ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "version": 1, }, "edits": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, }, "newText": "import { SomeType } from \"./decl.d.ts\";\n", }], }], }, }]) ); client.shutdown(); } #[test] fn lsp_code_actions_import_map_remap() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.json", json!({ "imports": { "foo": "./foo.ts", "bar": "./bar.ts", }, }) .to_string(), ); temp_dir.write("foo.ts", ""); temp_dir.write("bar.ts", ""); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#" import "./foo.ts"; import type {} from "./bar.ts"; "#, } })); let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap() }, "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 3, "character": 0 }, }, "context": { "diagnostics": diagnostics.all(), "only": ["quickfix"], }, }), ); assert_eq!( res, json!([ { "title": "Update \"./foo.ts\" to \"foo\" to use import map.", "kind": "quickfix", "diagnostics": [ { "range": { "start": { "line": 1, "character": 15 }, "end": { "line": 1, "character": 25 }, }, "severity": 4, "code": "import-map-remap", "source": "deno", "message": "The import specifier can be remapped to \"foo\" which will resolve it via the active import map.", "data": { "from": "./foo.ts", "to": "foo" }, }, ], "edit": { "changes": { temp_dir.url().join("file.ts").unwrap(): [ { "range": { "start": { "line": 1, "character": 15 }, "end": { "line": 1, "character": 25 }, }, "newText": "\"foo\"", }, ], }, }, }, { "title": "Update \"./bar.ts\" to \"bar\" to use import map.", "kind": "quickfix", "diagnostics": [ { "range": { "start": { "line": 2, "character": 28 }, "end": { "line": 2, "character": 38 }, }, "severity": 4, "code": "import-map-remap", "source": "deno", "message": "The import specifier can be remapped to \"bar\" which will resolve it via the active import map.", "data": { "from": "./bar.ts", "to": "bar" }, }, ], "edit": { "changes": { temp_dir.url().join("file.ts").unwrap(): [ { "range": { "start": { "line": 2, "character": 28 }, "end": { "line": 2, "character": 38 }, }, "newText": "\"bar\"", }, ], }, }, }, ]), ); client.shutdown(); } #[test] fn lsp_code_actions_refactor() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "var x: { a?: number; b?: string } = {};\n" } })); let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 1, "character": 0 } }, "context": { "diagnostics": [], "only": ["refactor"] } }), ); assert_eq!( res, json!([{ "title": "Move to a new file", "kind": "refactor.move.newFile", "isPreferred": false, "data": { "specifier": "file:///a/file.ts", "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 1, "character": 0 } }, "refactorName": "Move to a new file", "actionName": "Move to a new file" } }, { "title": "Extract to function in module scope", "kind": "refactor.extract.function", "isPreferred": false, "data": { "specifier": "file:///a/file.ts", "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 1, "character": 0 } }, "refactorName": "Extract Symbol", "actionName": "function_scope_0" } }, { "title": "Extract to constant in enclosing scope", "kind": "refactor.extract.constant", "isPreferred": false, "data": { "specifier": "file:///a/file.ts", "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 1, "character": 0 } }, "refactorName": "Extract Symbol", "actionName": "constant_scope_0" } }, { "title": "Convert default export to named export", "kind": "refactor.rewrite.export.named", "isPreferred": false, "disabled": { "reason": "This file already has a default export" }, "data": { "specifier": "file:///a/file.ts", "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 1, "character": 0 } }, "refactorName": "Convert export", "actionName": "Convert default export to named export" } }, { "title": "Convert named export to default export", "kind": "refactor.rewrite.export.default", "isPreferred": false, "disabled": { "reason": "This file already has a default export" }, "data": { "specifier": "file:///a/file.ts", "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 1, "character": 0 } }, "refactorName": "Convert export", "actionName": "Convert named export to default export" } }, { "title": "Convert namespace import to named imports", "kind": "refactor.rewrite.import.named", "isPreferred": false, "disabled": { "reason": "Selection is not an import declaration." }, "data": { "specifier": "file:///a/file.ts", "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 1, "character": 0 } }, "refactorName": "Convert import", "actionName": "Convert namespace import to named imports" } }, { "title": "Convert named imports to default import", "kind": "refactor.rewrite.import.default", "isPreferred": false, "disabled": { "reason": "Selection is not an import declaration." }, "data": { "specifier": "file:///a/file.ts", "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 1, "character": 0 } }, "refactorName": "Convert import", "actionName": "Convert named imports to default import" } }, { "title": "Convert named imports to namespace import", "kind": "refactor.rewrite.import.namespace", "isPreferred": false, "disabled": { "reason": "Selection is not an import declaration." }, "data": { "specifier": "file:///a/file.ts", "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 1, "character": 0 } }, "refactorName": "Convert import", "actionName": "Convert named imports to namespace import" } }]) ); let res = client.write_request( "codeAction/resolve", json!({ "title": "Extract to interface", "kind": "refactor.extract.interface", "isPreferred": true, "data": { "specifier": "file:///a/file.ts", "range": { "start": { "line": 0, "character": 7 }, "end": { "line": 0, "character": 33 } }, "refactorName": "Extract type", "actionName": "Extract to interface" } }), ); assert_eq!( res, json!({ "title": "Extract to interface", "kind": "refactor.extract.interface", "edit": { "documentChanges": [{ "textDocument": { "uri": "file:///a/file.ts", "version": 1 }, "edits": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "interface NewType {\n a?: number;\n b?: string;\n}\n\n" }, { "range": { "start": { "line": 0, "character": 7 }, "end": { "line": 0, "character": 33 } }, "newText": "NewType" }] }] }, "isPreferred": true, "data": { "specifier": "file:///a/file.ts", "range": { "start": { "line": 0, "character": 7 }, "end": { "line": 0, "character": 33 } }, "refactorName": "Extract type", "actionName": "Extract to interface" } }) ); client.shutdown(); } #[test] fn lsp_code_actions_imports_respects_fmt_config() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write( "./deno.jsonc", json!({ "fmt": { "semiColons": false, "singleQuote": true, } }) .to_string(), ); temp_dir.write( "file00.ts", r#" export interface MallardDuckConfigOptions extends DuckConfigOptions { kind: "mallard"; } "#, ); temp_dir.write( "file01.ts", r#" export interface DuckConfigOptions { kind: string; quacks: boolean; } "#, ); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file00.ts").unwrap(), "languageId": "typescript", "version": 1, "text": temp_dir.read_to_string("file00.ts"), } })); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file01.ts").unwrap(), "languageId": "typescript", "version": 1, "text": temp_dir.read_to_string("file01.ts"), } })); let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": temp_dir.url().join("file00.ts").unwrap() }, "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 4, "character": 0 } }, "context": { "diagnostics": [{ "range": { "start": { "line": 1, "character": 55 }, "end": { "line": 1, "character": 64 } }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'DuckConfigOptions'." }], "only": ["quickfix"] } }), ); assert_eq!( res, json!([{ "title": "Add import from \"./file01.ts\"", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 1, "character": 55 }, "end": { "line": 1, "character": 64 } }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'DuckConfigOptions'." }], "edit": { "documentChanges": [{ "textDocument": { "uri": temp_dir.url().join("file00.ts").unwrap(), "version": 1 }, "edits": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "import { DuckConfigOptions } from './file01.ts'\n" }] }] } }]) ); let res = client.write_request( "codeAction/resolve", json!({ "title": "Add all missing imports", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 1, "character": 55 }, "end": { "line": 1, "character": 64 } }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'DuckConfigOptions'." }], "data": { "specifier": temp_dir.url().join("file00.ts").unwrap(), "fixId": "fixMissingImport" } }), ); assert_eq!( res, json!({ "title": "Add all missing imports", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 1, "character": 55 }, "end": { "line": 1, "character": 64 } }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'DuckConfigOptions'." }], "edit": { "documentChanges": [{ "textDocument": { "uri": temp_dir.url().join("file00.ts").unwrap(), "version": 1 }, "edits": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "import { DuckConfigOptions } from './file01.ts'\n" }] }] }, "data": { "specifier": temp_dir.url().join("file00.ts").unwrap(), "fixId": "fixMissingImport" } }) ); client.shutdown(); } #[test] fn lsp_quote_style_from_workspace_settings() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write( "file00.ts", r#" export interface MallardDuckConfigOptions extends DuckConfigOptions { kind: "mallard"; } "#, ); temp_dir.write( "file01.ts", r#" export interface DuckConfigOptions { kind: string; quacks: boolean; } "#, ); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.change_configuration(json!({ "deno": { "enable": true, }, "typescript": { "preferences": { "quoteStyle": "single", }, }, })); let code_action_params = json!({ "textDocument": { "uri": temp_dir.url().join("file00.ts").unwrap(), }, "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 4, "character": 0 }, }, "context": { "diagnostics": [{ "range": { "start": { "line": 1, "character": 56 }, "end": { "line": 1, "character": 73 }, }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'DuckConfigOptions'.", }], "only": ["quickfix"], }, }); let res = client.write_request("textDocument/codeAction", code_action_params.clone()); // Expect single quotes in the auto-import. assert_eq!( res, json!([{ "title": "Add import from \"./file01.ts\"", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 1, "character": 56 }, "end": { "line": 1, "character": 73 }, }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'DuckConfigOptions'.", }], "edit": { "documentChanges": [{ "textDocument": { "uri": temp_dir.url().join("file00.ts").unwrap(), "version": null, }, "edits": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, }, "newText": "import { DuckConfigOptions } from './file01.ts';\n", }], }], }, }]), ); // It should ignore the workspace setting if a `deno.json` is present. temp_dir.write("./deno.json", json!({}).to_string()); client.did_change_watched_files(json!({ "changes": [{ "uri": temp_dir.url().join("deno.json").unwrap(), "type": 1, }], })); let res = client.write_request("textDocument/codeAction", code_action_params); // Expect double quotes in the auto-import. assert_eq!( res, json!([{ "title": "Add import from \"./file01.ts\"", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 1, "character": 56 }, "end": { "line": 1, "character": 73 }, }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'DuckConfigOptions'.", }], "edit": { "documentChanges": [{ "textDocument": { "uri": temp_dir.url().join("file00.ts").unwrap(), "version": null, }, "edits": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, }, "newText": "import { DuckConfigOptions } from \"./file01.ts\";\n", }], }], }, }]), ); client.shutdown(); } #[test] fn lsp_code_actions_refactor_no_disabled_support() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.with_capabilities(|c| { let doc = c.text_document.as_mut().unwrap(); let code_action = doc.code_action.as_mut().unwrap(); code_action.disabled_support = Some(false); }); }); client.did_open( 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 res = 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"] } }), ); assert_eq!( res, json!([{ "title": "Move to a new file", "kind": "refactor.move.newFile", "isPreferred": false, "data": { "specifier": "file:///a/file.ts", "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 14, "character": 0 } }, "refactorName": "Move to a new file", "actionName": "Move to a new file" } }, { "title": "Extract to function in module scope", "kind": "refactor.extract.function", "isPreferred": false, "data": { "specifier": "file:///a/file.ts", "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 14, "character": 0 } }, "refactorName": "Extract Symbol", "actionName": "function_scope_0" } }]) ); client.shutdown(); } #[test] fn lsp_code_actions_deadlock() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let large_file_text = fs::read_to_string(testdata_path().join("lsp").join("large_file.txt")) .unwrap(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "javascript", "version": 1, "text": large_file_text, } })); client.write_request( "textDocument/semanticTokens/full", json!({ "textDocument": { "uri": "file:///a/file.ts" } }), ); 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": "+++" } ] }), ); 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": "// " } ] }), ); 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" } ] }), ); // 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)); client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "position": { "line": 609, "character": 33, } }), ); client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "range": { "start": { "line": 441, "character": 33 }, "end": { "line": 441, "character": 42 } }, "context": { "diagnostics": [{ "range": { "start": { "line": 441, "character": 33 }, "end": { "line": 441, "character": 42 } }, "severity": 1, "code": 7031, "source": "deno-ts", "message": "Binding element 'debugFlag' implicitly has an 'any' type." }], "only": [ "quickfix" ] } }), ); client.read_diagnostics(); client.shutdown(); } #[test] fn lsp_completions() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "Deno." } })); let list = client.get_completion_list( "file:///a/file.ts", (0, 5), json!({ "triggerKind": 2, "triggerCharacter": "." }), ); assert!(!list.is_incomplete); assert!(list.items.len() > 90); let res = client.write_request( "completionItem/resolve", json!({ "label": "build", "kind": 6, "sortText": "1", "insertTextFormat": 1, "data": { "tsc": { "specifier": "file:///a/file.ts", "position": 5, "name": "build", "useCodeSnippet": false } } }), ); assert_eq!( res, json!({ "label": "build", "kind": 6, "detail": "const Deno.build: {\n target: string;\n arch: \"x86_64\" | \"aarch64\";\n os: \"darwin\" | \"linux\" | \"android\" | \"windows\" | \"freebsd\" | \"netbsd\" | \"aix\" | \"solaris\" | \"illumos\";\n vendor: string;\n env?: string;\n}", "documentation": { "kind": "markdown", "value": "Information related to the build of the current Deno runtime.\n\nUsers are discouraged from code branching based on this information, as\nassumptions about what is available in what build environment might change\nover time. Developers should specifically sniff out the features they\nintend to use.\n\nThe intended use for the information is for logging and debugging purposes.\n\n*@category* - Runtime" }, "sortText": "1", "insertTextFormat": 1 }) ); client.shutdown(); } #[test] fn lsp_completions_private_fields() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": r#"class Foo { #myProperty = "value"; constructor() { this.# } }"# } })); let list = client.get_completion_list( "file:///a/file.ts", (0, 57), json!({ "triggerKind": 1 }), ); assert_eq!(list.items.len(), 1); let item = &list.items[0]; assert_eq!(item.label, "#myProperty"); assert!(!list.is_incomplete); client.shutdown(); } #[test] fn lsp_completions_optional() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( 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 res = client.get_completion( "file:///a/file.ts", (8, 4), json!({ "triggerKind": 2, "triggerCharacter": "." }), ); assert_eq!( json!(res), 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 res = client.write_request( "completionItem/resolve", json!({ "label": "b?", "kind": 5, "sortText": "1", "filterText": "b", "insertText": "b", "data": { "tsc": { "specifier": "file:///a/file.ts", "position": 79, "name": "b", "useCodeSnippet": false } } }), ); assert_eq!( res, json!({ "label": "b?", "kind": 5, "detail": "(property) A.b?: string | undefined", "documentation": { "kind": "markdown", "value": "" }, "sortText": "1", "filterText": "b", "insertText": "b" }) ); client.shutdown(); } #[test] fn lsp_completions_auto_import() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": Url::parse("file:///a/🦕.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "/**\n *\n * @example\n * ```ts\n * const result = add(1, 2);\n * console.log(result); // 3\n * ```\n *\n * @param {number} a - The first number\n * @param {number} b - The second number\n */\nexport function add(a: number, b: number) {\n return a + b;\n}", } })); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "const result = add(1, 2);\n", } })); let list = client.get_completion_list( "file:///a/file.ts", (0, 18), json!({ "triggerKind": 1 }), ); assert!(!list.is_incomplete); let item = list.items.iter().find(|item| item.label == "add"); let Some(item) = item else { panic!("completions items missing 'add' symbol"); }; let res = client.write_request("completionItem/resolve", json!(item)); assert_eq!( res, json!({ "label": "add", "labelDetails": { "description": "./🦕.ts", }, "kind": 3, "detail": "function add(a: number, b: number): number", "documentation": { "kind": "markdown", "value": "\n\n*@example* \n```ts\nconst result = add(1, 2);\nconsole.log(result); // 3\n``` \n\n*@param* - a - The first number \n\n*@param* - b - The second number" }, "sortText": "￿16_0", "additionalTextEdits": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "import { add } from \"./🦕.ts\";\n\n" } ] }) ); client.shutdown(); } #[test] fn lsp_npm_completions_auto_import_and_quick_fix_no_import_map() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "import {getClient} from 'npm:@denotest/types-exports-subpaths@1/client';import chalk from 'npm:chalk@5.0';\n\n", } }), ); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [ ["npm:@denotest/types-exports-subpaths@1/client", "npm:chalk@5.0"], "file:///a/file.ts", ], }), ); // try auto-import with path client.did_open(json!({ "textDocument": { "uri": "file:///a/a.ts", "languageId": "typescript", "version": 1, "text": "getClie", } })); let list = client.get_completion_list( "file:///a/a.ts", (0, 7), json!({ "triggerKind": 1 }), ); assert!(!list.is_incomplete); let item = list .items .iter() .find(|item| item.label == "getClient") .unwrap(); let res = client.write_request("completionItem/resolve", item); assert_eq!( res, json!({ "label": "getClient", "labelDetails": { "description": "npm:@denotest/types-exports-subpaths@1/client", }, "kind": 3, "detail": "function getClient(): 5", "documentation": { "kind": "markdown", "value": "" }, "sortText": "￿16_1", "additionalTextEdits": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "import { getClient } from \"npm:@denotest/types-exports-subpaths@1/client\";\n\n" } ] }) ); // try quick fix with path let diagnostics = client.did_open(json!({ "textDocument": { "uri": "file:///a/b.ts", "languageId": "typescript", "version": 1, "text": "getClient", } })); let diagnostics = diagnostics .messages_with_file_and_source("file:///a/b.ts", "deno-ts") .diagnostics; let res = client.write_request( "textDocument/codeAction", json!(json!({ "textDocument": { "uri": "file:///a/b.ts" }, "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 9 } }, "context": { "diagnostics": diagnostics, "only": ["quickfix"] } })), ); assert_eq!( res, json!([{ "title": "Add import from \"npm:@denotest/types-exports-subpaths@1/client\"", "kind": "quickfix", "diagnostics": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 9 } }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'getClient'.", } ], "edit": { "documentChanges": [{ "textDocument": { "uri": "file:///a/b.ts", "version": 1, }, "edits": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "import { getClient } from \"npm:@denotest/types-exports-subpaths@1/client\";\n\n" }] }] } }]) ); // try auto-import without path client.did_open(json!({ "textDocument": { "uri": "file:///a/c.ts", "languageId": "typescript", "version": 1, "text": "chal", } })); let list = client.get_completion_list( "file:///a/c.ts", (0, 4), json!({ "triggerKind": 1 }), ); assert!(!list.is_incomplete); let item = list .items .iter() .find(|item| item.label == "chalk") .unwrap(); let mut res = client.write_request("completionItem/resolve", item); let obj = res.as_object_mut().unwrap(); obj.remove("detail"); // not worth testing these obj.remove("documentation"); assert_eq!( res, json!({ "label": "chalk", "labelDetails": { "description": "npm:chalk@5.0", }, "kind": 6, "sortText": "￿16_1", "additionalTextEdits": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "import chalk from \"npm:chalk@5.0\";\n\n" } ] }) ); // try quick fix without path let diagnostics = client.did_open(json!({ "textDocument": { "uri": "file:///a/d.ts", "languageId": "typescript", "version": 1, "text": "chalk", } })); let diagnostics = diagnostics .messages_with_file_and_source("file:///a/d.ts", "deno-ts") .diagnostics; let res = client.write_request( "textDocument/codeAction", json!(json!({ "textDocument": { "uri": "file:///a/d.ts" }, "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 5 } }, "context": { "diagnostics": diagnostics, "only": ["quickfix"] } })), ); assert_eq!( res, json!([{ "title": "Add import from \"npm:chalk@5.0\"", "kind": "quickfix", "diagnostics": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 5 } }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'chalk'.", } ], "edit": { "documentChanges": [{ "textDocument": { "uri": "file:///a/d.ts", "version": 1, }, "edits": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "import chalk from \"npm:chalk@5.0\";\n\n" }] }] } }]) ); client.shutdown(); } #[test] fn lsp_npm_auto_import_and_quick_fix_byonm() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .add_npm_env_vars() .build(); let temp_dir = context.temp_dir(); temp_dir.write("deno.json", json!({}).to_string()); temp_dir.write( "package.json", json!({ "dependencies": { "cowsay": "*", }, }) .to_string(), ); context .new_command() .args("install") .run() .skip_output_check(); temp_dir.write("other.ts", "import \"cowsay\";\n"); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "think({ text: \"foo\" });\n", }, })); let list = client.get_completion_list( temp_dir.url().join("file.ts").unwrap(), (0, 5), json!({ "triggerKind": 1 }), ); assert!(!list.is_incomplete); let item = list .items .iter() .find(|item| item.label == "think") .unwrap(); let res = client.write_request("completionItem/resolve", item); assert_eq!( res, json!({ "label": "think", "labelDetails": { "description": "cowsay", }, "kind": 3, "detail": "function think(options: IOptions): string", "documentation": { "kind": "markdown", "value": "\n\n*@param* \noptions ## Face :\nEither choose a mode (set the value as true) **_or_**\nset your own defined eyes and tongue to `e` and `T`.\n- ### `e` : eyes\n- ### `T` : tongue\n\n## Cow :\nEither specify a cow name (e.g. \"fox\") **_or_**\nset the value of `r` to true which selects a random cow.\n- ### `r` : random selection\n- ### `f` : cow name - from `cows` folder\n\n## Modes :\nModes are just ready-to-use faces, here's their list:\n- #### `b` : borg\n- #### `d` : dead \n- #### `g` : greedy\n- #### `p` : paranoia\n- #### `s` : stoned\n- #### `t` : tired\n- #### `w` : youthful\n- #### `y` : wired \n\n*@example* \n```\n// custom cow and face\ncowsay.think({\n text: 'Hello world!',\n e: '^^', // eyes\n T: 'U ', // tongue\n f: 'USA' // name of the cow from `cows` folder\n})\n\n// using a random cow\ncowsay.think({\n text: 'Hello world!',\n e: 'xx', // eyes\n r: true, // random mode - use a random cow.\n})\n\n// using a mode\ncowsay.think({\n text: 'Hello world!',\n y: true, // using y mode - youthful mode\n})\n```", }, "sortText": "￿16_0", "additionalTextEdits": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, }, "newText": "import { think } from \"cowsay\";\n\n", }, ], }), ); let diagnostics = diagnostics .messages_with_file_and_source( temp_dir.url().join("file.ts").unwrap().as_str(), "deno-ts", ) .diagnostics; let res = client.write_request( "textDocument/codeAction", json!(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), }, "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 5 }, }, "context": { "diagnostics": &diagnostics, "only": ["quickfix"], }, })), ); assert_eq!( res, json!([ { "title": "Add import from \"cowsay\"", "kind": "quickfix", "diagnostics": &diagnostics, "edit": { "documentChanges": [{ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "version": 1, }, "edits": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, }, "newText": "import { think } from \"cowsay\";\n\n", }], }], }, }, { "title": "Add missing function declaration 'think'", "kind": "quickfix", "diagnostics": &diagnostics, "edit": { "documentChanges": [ { "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "version": 1, }, "edits": [ { "range": { "start": { "line": 1, "character": 0 }, "end": { "line": 1, "character": 0 }, }, "newText": "\nfunction think(arg0: { text: string; }) {\n throw new Error(\"Function not implemented.\");\n}\n", }, ], }, ], }, }, ]), ); client.shutdown(); } #[test] fn lsp_npm_auto_import_with_deno_types() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .add_npm_env_vars() .build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.json", json!({ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "react", "jsxImportSourceTypes": "@types/react", }, }) .to_string(), ); temp_dir.write( "package.json", json!({ "dependencies": { "react": "*", "@types/react": "*", "lz-string": "1.3", "@types/lz-string": "1.3", }, }) .to_string(), ); context.run_npm("install"); temp_dir.write( "other.ts", r#" // @deno-types="@types/lz-string" import "lz-string"; "#, ); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#" compressToBase64(); createRef(); "#, }, })); let list = client.get_completion_list( temp_dir.url().join("file.ts").unwrap(), (1, 24), json!({ "triggerKind": 1 }), ); let item = list .items .iter() .find(|item| item.label == "compressToBase64") .unwrap(); let res = client.write_request("completionItem/resolve", item); assert_eq!( res, json!({ "label": "compressToBase64", "labelDetails": { "description": "lz-string", }, "kind": 2, "detail": "(method) LZString.LZStringStatic.compressToBase64(uncompressed: string): string", "documentation": { "kind": "markdown", "value": "Compresses input string producing an instance of a ASCII UTF-16 string,\nwhich represents the original string encoded in Base64.\nThe result can be safely transported outside the browser with a\nguarantee that none of the characters produced need to be URL-encoded.\n\n*@param* - uncompressed A string which should be compressed.", }, "sortText": "￿16_0", "additionalTextEdits": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, }, "newText": "// @deno-types=\"@types/lz-string\"\nimport { compressToBase64 } from \"lz-string\";\n", }, ], }), ); let list = client.get_completion_list( temp_dir.url().join("file.ts").unwrap(), (2, 17), json!({ "triggerKind": 1 }), ); let item = list .items .iter() .find(|item| item.label == "createRef") .unwrap(); let res = client.write_request("completionItem/resolve", item); assert_eq!( res, json!({ "label": "createRef", "labelDetails": { "description": "react", }, "kind": 3, "detail": "function React.createRef(): React.RefObject", "documentation": { "kind": "markdown", "value": "" }, "sortText": "￿16_0", "additionalTextEdits": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, }, "newText": "// @deno-types=\"@types/react\"\nimport { createRef } from \"react\";\n", }, ], }), ); client.shutdown(); } #[test] fn lsp_completions_node_specifier() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import fs from \"node:as\";\n", }, })); let list = client.get_completion_list( temp_dir.url().join("file.ts").unwrap(), (0, 23), json!({ "triggerKind": 2, "triggerCharacter": ".", }), ); assert!(!list.is_incomplete); assert_eq!( list .items .iter() .map(|i| i.label.as_str()) .collect::>(), vec![ "node:assert", "node:assert/strict", "node:async_hooks", "node:buffer", "node:child_process", "node:cluster", "node:console", "node:constants", "node:crypto", "node:dgram", "node:diagnostics_channel", "node:dns", "node:dns/promises", "node:domain", "node:events", "node:fs", "node:fs/promises", "node:http", "node:http2", "node:https", "node:inspector", "node:module", "node:net", "node:os", "node:path", "node:path/posix", "node:path/win32", "node:perf_hooks", "node:process", "node:punycode", "node:querystring", "node:repl", "node:readline", "node:readline/promises", "node:stream", "node:stream/consumers", "node:stream/promises", "node:stream/web", "node:string_decoder", "node:sys", "node:test", "node:timers", "node:timers/promises", "node:tls", "node:tty", "node:url", "node:util", "node:util/types", "node:v8", "node:vm", "node:wasi", "node:worker_threads", "node:zlib", ], ); for item in &list.items { let specifier = item.label.as_str(); assert_eq!( json!(item), json!({ "label": specifier, "kind": 17, "detail": "(node)", "textEdit": { "range": { "start": { "line": 0, "character": 16 }, "end": { "line": 0, "character": 23 }, }, "newText": specifier, }, "commitCharacters": ["\"", "'"], }), ); } client.shutdown(); } #[test] fn lsp_infer_return_type() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write("deno.json", json!({}).to_string()); temp_dir.write( "types.d.ts", r#" export interface SomeInterface { someField: number; } declare global { export function someFunction(): SomeInterface; } "#, ); let file = source_file( temp_dir.path().join("file.ts"), r#" function foo() { return someFunction(); } foo(); "#, ); let mut client = context.new_lsp_command().build(); client.initialize_default(); let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": file.url() }, "range": { "start": { "line": 1, "character": 15 }, "end": { "line": 1, "character": 18 }, }, "context": { "diagnostics": [], "only": ["refactor.rewrite.function.returnType"], } }), ); assert_eq!( &res, &json!([ { "title": "Infer function return type", "kind": "refactor.rewrite.function.returnType", "isPreferred": false, "data": { "specifier": file.url(), "range": { "start": { "line": 1, "character": 15 }, "end": { "line": 1, "character": 18 }, }, "refactorName": "Infer function return type", "actionName": "Infer function return type", }, } ]), ); let code_action = res.as_array().unwrap().first().unwrap(); let res = client.write_request("codeAction/resolve", code_action); assert_eq!( &res, &json!({ "title": "Infer function return type", "kind": "refactor.rewrite.function.returnType", "isPreferred": false, "data": { "specifier": file.url(), "range": { "start": { "line": 1, "character": 15 }, "end": { "line": 1, "character": 18 }, }, "refactorName": "Infer function return type", "actionName": "Infer function return type", }, "edit": { "documentChanges": [ { "textDocument": { "uri": file.url(), "version": null }, "edits": [ { "range": { "start": { "line": 1, "character": 20 }, "end": { "line": 1, "character": 20 }, }, "newText": ": import(\"./types.d.ts\").SomeInterface", }, ], }, ], }, }), ); client.shutdown(); } // Regression test for https://github.com/denoland/deno/issues/23895. #[test] fn lsp_npm_types_nested_js_dts() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); let file = source_file( temp_dir.path().join("file.ts"), r#" import { someString } from "npm:@denotest/types-nested-js-dts"; const someNumber: number = someString; console.log(someNumber); "#, ); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], file.url()], }), ); let diagnostics = client.did_open_file(&file); assert_eq!( json!(diagnostics.all()), json!([ { "range": { "start": { "line": 2, "character": 12, }, "end": { "line": 2, "character": 22, }, }, "severity": 1, "code": 2322, "source": "deno-ts", "message": "Type 'string' is not assignable to type 'number'.", }, ]) ); } #[test] fn lsp_completions_using_decl() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": r#"function makeResource() { return { [Symbol.dispose]() { }, }; } using resource = makeResource(); res"# } })); let list = client.get_completion_list( "file:///a/file.ts", (9, 3), json!({ "triggerKind": 2, "triggerCharacter": "." }), ); assert!(list.items.iter().any(|i| i.label == "resource")); assert!(!list.is_incomplete); client.shutdown(); } #[test] fn lsp_npm_always_caches() { // npm specifiers should always be cached even when not specified // because they affect the resolution of each other let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir_path = context.temp_dir().path(); // this file should be auto-discovered by the lsp let not_opened_file = temp_dir_path.join("not_opened.ts"); not_opened_file.write("import chalk from 'npm:chalk@5.0';\n"); // create the lsp and cache a different npm specifier let mut client = context.new_lsp_command().build(); client.initialize_default(); let opened_file_uri = temp_dir_path.join("file.ts").url_file(); client.did_open( json!({ "textDocument": { "uri": opened_file_uri, "languageId": "typescript", "version": 1, "text": "import {getClient} from 'npm:@denotest/types-exports-subpaths@1/client';\n", } }), ); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [ ["npm:@denotest/types-exports-subpaths@1/client"], opened_file_uri, ], }), ); // now open a new file and chalk should be working let new_file_uri = temp_dir_path.join("new_file.ts").url_file(); client.did_open(json!({ "textDocument": { "uri": new_file_uri, "languageId": "typescript", "version": 1, "text": "import chalk from 'npm:chalk@5.0';\nchalk.", } })); let list = client.get_completion_list( new_file_uri, (1, 6), json!({ "triggerKind": 2, "triggerCharacter": "." }), ); assert!(!list.is_incomplete); assert!(list.items.iter().any(|i| i.label == "green")); assert!(list.items.iter().any(|i| i.label == "red")); client.shutdown(); } #[test] fn lsp_semantic_tokens_for_disabled_module() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize_with_config( |builder| { builder.set_deno_enable(false); }, json!({ "deno": { "enable": false } }), ); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "const someConst = 1; someConst" } })); let res = client.write_request( "textDocument/semanticTokens/full", json!({ "textDocument": { "uri": "file:///a/file.ts" } }), ); assert_eq!( res, json!({ "data": [0, 6, 9, 7, 9, 0, 15, 9, 7, 8], }) ); client.shutdown(); } #[test] fn lsp_completions_auto_import_and_quick_fix_with_import_map() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); let import_map = r#"{ "imports": { "print_hello": "http://localhost:4545/subdir/print_hello.ts", "chalk": "npm:chalk@~5", "nested/": "npm:/@denotest/types-exports-subpaths@1/nested/", "types-exports-subpaths/": "npm:/@denotest/types-exports-subpaths@1/" } }"#; temp_dir.write("import_map.json", import_map); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_import_map("import_map.json"); }); client.did_open( json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": concat!( "import {getClient} from 'npm:@denotest/types-exports-subpaths@1/client';\n", "import _test1 from 'npm:chalk@^5.0';\n", "import chalk from 'npm:chalk@~5';\n", "import chalk from 'npm:chalk@~5';\n", "import {entryB} from 'npm:@denotest/types-exports-subpaths@1/nested/entry-b';\n", "import {printHello} from 'print_hello';\n", "\n", ), } }), ); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [ [ "npm:@denotest/types-exports-subpaths@1/client", "npm:@denotest/types-exports-subpaths@1/nested/entry-b", "npm:chalk@^5.0", "npm:chalk@~5", "http://localhost:4545/subdir/print_hello.ts", ], temp_dir.url().join("file.ts").unwrap(), ], }), ); // try auto-import with path client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("a.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "getClie", } })); let list = client.get_completion_list( temp_dir.url().join("a.ts").unwrap(), (0, 7), json!({ "triggerKind": 1 }), ); assert!(!list.is_incomplete); let item = list .items .iter() .find(|item| item.label == "getClient") .unwrap(); let res = client.write_request("completionItem/resolve", item); assert_eq!( res, json!({ "label": "getClient", "labelDetails": { "description": "types-exports-subpaths/client", }, "kind": 3, "detail": "function getClient(): 5", "documentation": { "kind": "markdown", "value": "" }, "sortText": "￿16_0", "additionalTextEdits": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "import { getClient } from \"types-exports-subpaths/client\";\n\n" } ] }) ); // try quick fix with path let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("b.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "getClient", } })); let diagnostics = diagnostics .messages_with_file_and_source( temp_dir.url().join("b.ts").unwrap().as_str(), "deno-ts", ) .diagnostics; let res = client.write_request( "textDocument/codeAction", json!(json!({ "textDocument": { "uri": temp_dir.url().join("b.ts").unwrap() }, "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 9 } }, "context": { "diagnostics": diagnostics, "only": ["quickfix"] } })), ); assert_eq!( res, json!([{ "title": "Add import from \"types-exports-subpaths/client\"", "kind": "quickfix", "diagnostics": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 9 } }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'getClient'.", } ], "edit": { "documentChanges": [{ "textDocument": { "uri": temp_dir.url().join("b.ts").unwrap(), "version": 1, }, "edits": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "import { getClient } from \"types-exports-subpaths/client\";\n\n" }] }] } }]) ); // try auto-import without path client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("c.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "chal", } })); let list = client.get_completion_list( temp_dir.url().join("c.ts").unwrap(), (0, 4), json!({ "triggerKind": 1 }), ); assert!(!list.is_incomplete); let item = list .items .iter() .find(|item| item.label == "chalk") .unwrap(); let mut res = client.write_request("completionItem/resolve", item); let obj = res.as_object_mut().unwrap(); obj.remove("detail"); // not worth testing these obj.remove("documentation"); assert_eq!( res, json!({ "label": "chalk", "labelDetails": { "description": "chalk", }, "kind": 6, "sortText": "￿16_0", "additionalTextEdits": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "import chalk from \"chalk\";\n\n" } ] }) ); // try quick fix without path let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("d.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "chalk", } })); let diagnostics = diagnostics .messages_with_file_and_source( temp_dir.url().join("d.ts").unwrap().as_str(), "deno-ts", ) .diagnostics; let res = client.write_request( "textDocument/codeAction", json!(json!({ "textDocument": { "uri": temp_dir.url().join("d.ts").unwrap() }, "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 5 } }, "context": { "diagnostics": diagnostics, "only": ["quickfix"] } })), ); assert_eq!( res, json!([{ "title": "Add import from \"chalk\"", "kind": "quickfix", "diagnostics": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 5 } }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'chalk'.", } ], "edit": { "documentChanges": [{ "textDocument": { "uri": temp_dir.url().join("d.ts").unwrap(), "version": 1, }, "edits": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "import chalk from \"chalk\";\n\n" }] }] } }]) ); // try auto-import with http import map client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("e.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "printH", } })); let list = client.get_completion_list( temp_dir.url().join("e.ts").unwrap(), (0, 6), json!({ "triggerKind": 1 }), ); assert!(!list.is_incomplete); let item = list .items .iter() .find(|item| item.label == "printHello") .unwrap(); let mut res = client.write_request("completionItem/resolve", item); let obj = res.as_object_mut().unwrap(); obj.remove("detail"); // not worth testing these obj.remove("documentation"); assert_eq!( res, json!({ "label": "printHello", "labelDetails": { "description": "print_hello", }, "kind": 3, "sortText": "￿16_0", "additionalTextEdits": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "import { printHello } from \"print_hello\";\n\n" } ] }) ); // try quick fix with http import let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("f.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "printHello", } })); let diagnostics = diagnostics .messages_with_file_and_source( temp_dir.url().join("f.ts").unwrap().as_str(), "deno-ts", ) .diagnostics; let res = client.write_request( "textDocument/codeAction", json!(json!({ "textDocument": { "uri": temp_dir.url().join("f.ts").unwrap() }, "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 10 } }, "context": { "diagnostics": diagnostics, "only": ["quickfix"] } })), ); assert_eq!( res, json!([{ "title": "Add import from \"print_hello\"", "kind": "quickfix", "diagnostics": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 10 } }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'printHello'.", } ], "edit": { "documentChanges": [{ "textDocument": { "uri": temp_dir.url().join("f.ts").unwrap(), "version": 1, }, "edits": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "import { printHello } from \"print_hello\";\n\n" }] }] } }]) ); // try auto-import with npm package with sub-path on value side of import map client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("nested_path.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "entry", } })); let list = client.get_completion_list( temp_dir.url().join("nested_path.ts").unwrap(), (0, 5), json!({ "triggerKind": 1 }), ); assert!(!list.is_incomplete); let item = list .items .iter() .find(|item| item.label == "entryB") .unwrap(); let res = client.write_request("completionItem/resolve", item); assert_eq!( res, json!({ "label": "entryB", "labelDetails": { "description": "nested/entry-b", }, "kind": 3, "detail": "function entryB(): \"b\"", "documentation": { "kind": "markdown", "value": "" }, "sortText": "￿16_0", "additionalTextEdits": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "import { entryB } from \"nested/entry-b\";\n\n" } ] }) ); client.shutdown(); } // Regression test for https://github.com/denoland/deno/issues/25775. #[test] fn lsp_quick_fix_missing_import_exclude_bare_node_builtins() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .add_npm_env_vars() .build(); let temp_dir = context.temp_dir(); temp_dir.write( "package.json", json!({ "dependencies": { "@types/node": "*", }, }) .to_string(), ); context.run_npm("install"); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, // Include node:buffer import to ensure @types/node is in the dep graph. "text": "import \"node:buffer\";\nassert();\n", }, })); let diagnostic = diagnostics .all() .into_iter() .find(|d| d.message == "Cannot find name 'assert'.") .unwrap(); let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), }, "range": { "start": { "line": 1, "character": 0 }, "end": { "line": 1, "character": 6 }, }, "context": { "diagnostics": [&diagnostic], "only": ["quickfix"], }, }), ); let code_actions = serde_json::from_value::>(res).unwrap(); let titles = code_actions .iter() .map(|a| a.title.clone()) .collect::>(); assert_eq!( json!(titles), json!([ "Add import from \"node:assert\"", "Add import from \"node:console\"", "Add missing function declaration 'assert'", ]), ); client.shutdown(); } #[test] fn lsp_completions_snippet() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( 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 >(), vec![ "'chalk' is declared but its value is never read.", "Identifier expected." ] ); let list = client.get_completion_list( "file:///a/file.ts", (2, 11), json!({ "triggerKind": 2, "triggerCharacter": "." }), ); assert!(!list.is_incomplete); assert_eq!( list .items .iter() .map(|i| i.label.as_str()) .collect::>(), vec!["default", "MyClass", "named"] ); let res = client.write_request( "completionItem/resolve", json!({ "label": "MyClass", "kind": 6, "sortText": "1", "insertTextFormat": 1, "data": { "tsc": { "specifier": "file:///a/file.ts", "position": 69, "name": "MyClass", "useCodeSnippet": false } } }), ); assert_eq!( res, json!({ "label": "MyClass", "kind": 6, "sortText": "1", "insertTextFormat": 1, "data": { "tsc": { "specifier": "file:///a/file.ts", "position": 69, "name": "MyClass", "useCodeSnippet": false } } }) ); // 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." } ] }), ); client.read_diagnostics(); let list = client.get_completion_list( "file:///a/file.ts", (2, 6), json!({ "triggerKind": 2, "triggerCharacter": "." }), ); assert!(!list.is_incomplete); assert!(list.items.iter().any(|i| i.label == "green")); assert!(list.items.iter().any(|i| i.label == "red")); client.shutdown(); } #[test] fn lsp_npm_specifier_unopened_file() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); // create other.ts, which re-exports an npm specifier temp_dir.write( "other.ts", "export { default as chalk } from 'npm:chalk@5';", ); let mut client = context.new_lsp_command().build(); client.initialize_default(); // cache the other.ts file to the DENO_DIR let deno = deno_cmd_with_deno_dir(client.deno_dir()) .current_dir(temp_dir.path()) .arg("cache") .arg("--quiet") .arg("other.ts") .envs(env_vars_for_npm_tests()) .piped_output() .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_eq!(stdout.as_str(), ""); let stderr = String::from_utf8(output.stderr).unwrap(); assert_eq!(stderr.as_str(), ""); // open main.ts, which imports other.ts (unopened) client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("main.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import { chalk } from './other.ts';\n\n", } })); client.write_notification( "textDocument/didChange", json!({ "textDocument": { "uri": temp_dir.url().join("main.ts").unwrap(), "version": 2 }, "contentChanges": [ { "range": { "start": { "line": 2, "character": 0 }, "end": { "line": 2, "character": 0 } }, "text": "chalk." } ] }), ); client.read_diagnostics(); // now ensure completions work let list = client.get_completion_list( temp_dir.url().join("main.ts").unwrap(), (2, 6), json!({ "triggerKind": 2, "triggerCharacter": "." }), ); assert!(!list.is_incomplete); assert_eq!(list.items.len(), 63); assert!(list.items.iter().any(|i| i.label == "ansi256")); client.shutdown(); } #[test] fn lsp_completions_node_builtin() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "import fs from 'node:non-existent';\n\n", } })); let non_existent_diagnostics = diagnostics .messages_with_file_and_source("file:///a/file.ts", "deno") .diagnostics .into_iter() .filter(|d| { d.code == Some(lsp::NumberOrString::String("resolver-error".to_string())) }) .collect::>(); assert_eq!( json!(non_existent_diagnostics), json!([ { "range": { "start": { "line": 0, "character": 15 }, "end": { "line": 0, "character": 34 }, }, "severity": 1, "code": "resolver-error", "source": "deno", "message": "Unknown Node built-in module: non-existent" } ]) ); // update to have fs import client.write_notification( "textDocument/didChange", json!({ "textDocument": { "uri": "file:///a/file.ts", "version": 2 }, "contentChanges": [ { "range": { "start": { "line": 0, "character": 16 }, "end": { "line": 0, "character": 33 }, }, "text": "fs" } ] }), ); let diagnostics = client.read_diagnostics(); let diagnostics = diagnostics .messages_with_file_and_source("file:///a/file.ts", "deno") .diagnostics .into_iter() .filter(|d| { d.code == Some(lsp::NumberOrString::String( "import-node-prefix-missing".to_string(), )) }) .collect::>(); // get the quick fixes let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "range": { "start": { "line": 0, "character": 16 }, "end": { "line": 0, "character": 18 }, }, "context": { "diagnostics": json!(diagnostics), "only": ["quickfix"] } }), ); assert_eq!( res, json!([{ "title": "Update specifier to node:fs", "kind": "quickfix", "diagnostics": [ { "range": { "start": { "line": 0, "character": 15 }, "end": { "line": 0, "character": 19 } }, "severity": 1, "code": "import-node-prefix-missing", "source": "deno", "message": "Relative import path \"fs\" not prefixed with / or ./ or ../\n \u{1b}[0m\u{1b}[36mhint:\u{1b}[0m If you want to use a built-in Node module, add a \"node:\" prefix (ex. \"node:fs\").", "data": { "specifier": "fs" }, } ], "edit": { "changes": { "file:///a/file.ts": [ { "range": { "start": { "line": 0, "character": 15 }, "end": { "line": 0, "character": 19 } }, "newText": "\"node:fs\"" } ] } } }]) ); // update to have node:fs import client.write_notification( "textDocument/didChange", json!({ "textDocument": { "uri": "file:///a/file.ts", "version": 3, }, "contentChanges": [ { "range": { "start": { "line": 0, "character": 15 }, "end": { "line": 0, "character": 19 }, }, "text": "\"node:fs\"", } ] }), ); let diagnostics = client.read_diagnostics(); let cache_diagnostics = diagnostics .messages_with_file_and_source("file:///a/file.ts", "deno") .diagnostics .into_iter() .filter(|d| { d.code == Some(lsp::NumberOrString::String("not-installed-npm".to_string())) }) .collect::>(); assert_eq!( json!(cache_diagnostics), json!([ { "range": { "start": { "line": 0, "character": 15 }, "end": { "line": 0, "character": 24 } }, "data": { "specifier": "npm:@types/node", }, "severity": 1, "code": "not-installed-npm", "source": "deno", "message": "NPM package \"@types/node\" is not installed or doesn't exist." } ]) ); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [["npm:@types/node"], "file:///a/file.ts"], }), ); client.write_notification( "textDocument/didChange", json!({ "textDocument": { "uri": "file:///a/file.ts", "version": 4 }, "contentChanges": [ { "range": { "start": { "line": 2, "character": 0 }, "end": { "line": 2, "character": 0 } }, "text": "fs." } ] }), ); client.read_diagnostics(); let list = client.get_completion_list( "file:///a/file.ts", (2, 3), json!({ "triggerKind": 2, "triggerCharacter": "." }), ); assert!(!list.is_incomplete); assert!(list.items.iter().any(|i| i.label == "writeFile")); assert!(list.items.iter().any(|i| i.label == "writeFileSync")); client.shutdown(); } #[test] fn lsp_completions_node_specifier_node_modules_dir() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.write( temp_dir.path().join("deno.json"), json!({ "nodeModulesDir": "auto", }) .to_string(), ); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import fs from \"node:fs\";\n", }, })); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], temp_dir.url().join("file.ts").unwrap()], }), ); client.write_notification( "textDocument/didChange", json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "version": 2, }, "contentChanges": [ { "range": { "start": { "line": 1, "character": 0 }, "end": { "line": 1, "character": 0 }, }, "text": "fs.", }, ], }), ); let list = client.get_completion_list( temp_dir.url().join("file.ts").unwrap(), (1, 3), json!({ "triggerKind": 2, "triggerCharacter": ".", }), ); assert!(!list.is_incomplete); assert!(list.items.iter().any(|i| i.label == "writeFile")); assert!(list.items.iter().any(|i| i.label == "writeFileSync")); client.shutdown(); } #[test] fn lsp_completions_registry() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.add_test_server_suggestions(); }); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "import * as a from \"http://localhost:4545/x/a@\"" } })); let list = client.get_completion_list( "file:///a/file.ts", (0, 46), json!({ "triggerKind": 2, "triggerCharacter": "@" }), ); assert!(!list.is_incomplete); assert_eq!(list.items.len(), 3); let res = client.write_request( "completionItem/resolve", json!({ "label": "v2.0.0", "kind": 19, "detail": "(version)", "sortText": "0000000003", "filterText": "http://localhost:4545/x/a@v2.0.0", "textEdit": { "range": { "start": { "line": 0, "character": 20 }, "end": { "line": 0, "character": 46 } }, "newText": "http://localhost:4545/x/a@v2.0.0" } }), ); assert_eq!( res, json!({ "label": "v2.0.0", "kind": 19, "detail": "(version)", "sortText": "0000000003", "filterText": "http://localhost:4545/x/a@v2.0.0", "textEdit": { "range": { "start": { "line": 0, "character": 20 }, "end": { "line": 0, "character": 46 } }, "newText": "http://localhost:4545/x/a@v2.0.0" } }) ); client.shutdown(); } #[test] fn lsp_completions_registry_empty() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.add_test_server_suggestions(); }); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "import * as a from \"\"" } })); let res = client.get_completion( "file:///a/file.ts", (0, 20), json!({ "triggerKind": 2, "triggerCharacter": "\"" }), ); assert_eq!( json!(res), json!({ "isIncomplete": false, "items": [{ "label": ".", "kind": 19, "detail": "(local)", "sortText": "1", "insertText": ".", "commitCharacters": ["\"", "'"] }, { "label": "..", "kind": 19, "detail": "(local)", "sortText": "1", "insertText": "..", "commitCharacters": ["\"", "'" ] }, { "label": "http://localhost:4545", "kind": 19, "detail": "(registry)", "sortText": "2", "textEdit": { "range": { "start": { "line": 0, "character": 20 }, "end": { "line": 0, "character": 20 } }, "newText": "http://localhost:4545" }, "commitCharacters": ["\"", "'"] }] }) ); client.shutdown(); } #[test] fn lsp_auto_discover_registry() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "import * as a from \"http://localhost:4545/x/a@\"" } })); client.get_completion( "file:///a/file.ts", (0, 46), json!({ "triggerKind": 2, "triggerCharacter": "@" }), ); let res = client .read_notification_with_method::("deno/registryState") .unwrap(); assert_eq!( res, json!({ "origin": "http://localhost:4545", "suggestions": true, }), ); client.shutdown(); } #[test] fn lsp_cache_location() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_cache(".cache").add_test_server_suggestions(); }); client.did_open(json!({ "textDocument": { "uri": "file:///a/file_01.ts", "languageId": "typescript", "version": 1, "text": "export const a = \"a\";\n", } })); let diagnostics = client.did_open(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// @deno-types=\"http://127.0.0.1:4545/type_definitions/foo.d.ts\"\nimport * as b from \"http://127.0.0.1:4545/type_definitions/foo.js\";\nimport * as c from \"http://127.0.0.1:4545/subdir/type_reference.js\";\nimport * as d from \"http://127.0.0.1:4545/subdir/mod1.ts\";\nimport * as e from \"data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=\";\nimport * as f from \"./file_01.ts\";\nimport * as g from \"http://localhost:4545/x/a/mod.ts\";\n\nconsole.log(a, b, c, d, e, f, g);\n" } })); assert_eq!(diagnostics.all().len(), 6); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], "file:///a/file.ts"], }), ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "position": { "line": 0, "character": 28 } }), ); assert_eq!( res, 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 res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "position": { "line": 7, "character": 28 } }), ); assert_eq!( res, 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()); // not created because no emitting has occurred client.shutdown(); } /// 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 context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder .set_suggest_imports_hosts(vec![ ("http://localhost:4545/".to_string(), true), ("https://localhost:5545/".to_string(), true), ]) .set_tls_certificate(""); }); client.did_open(json!({ "textDocument": { "uri": "file:///a/file_01.ts", "languageId": "typescript", "version": 1, "text": "export const a = \"a\";\n", } })); let diagnostics = client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "import * as a from \"https://localhost:5545/xTypeScriptTypes.js\";\n// @deno-types=\"https://localhost:5545/type_definitions/foo.d.ts\"\nimport * as b from \"https://localhost:5545/type_definitions/foo.js\";\nimport * as c from \"https://localhost:5545/subdir/type_reference.js\";\nimport * as d from \"https://localhost:5545/subdir/mod1.ts\";\nimport * as e from \"data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=\";\nimport * as f from \"./file_01.ts\";\nimport * as g from \"http://localhost:4545/x/a/mod.ts\";\n\nconsole.log(a, b, c, d, e, f, g);\n" } })); let diagnostics = diagnostics.all(); assert_eq!(diagnostics.len(), 6); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], "file:///a/file.ts"], }), ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "position": { "line": 0, "character": 28 } }), ); assert_eq!( res, 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 res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "position": { "line": 7, "character": 28 } }), ); assert_eq!( res, 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 } } }) ); client.shutdown(); } #[test] fn lsp_npmrc() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .add_npm_env_vars() .build(); let temp_dir = context.temp_dir(); temp_dir.write( temp_dir.path().join("deno.json"), json!({ "nodeModulesDir": "auto", }) .to_string(), ); temp_dir.write( temp_dir.path().join("package.json"), json!({ "name": "npmrc_test", "version": "0.0.1", "dependencies": { "@denotest/basic": "1.0.0", }, }) .to_string(), ); temp_dir.write( temp_dir.path().join(".npmrc"), "\ @denotest:registry=http://localhost:4261/ //localhost:4261/:_authToken=private-reg-token ", ); let file = source_file( temp_dir.path().join("main.ts"), r#" import { getValue, setValue } from "@denotest/basic"; setValue(42); const n: string = getValue(); console.log(n); "#, ); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], file.url()], }), ); let diagnostics = client.did_open_file(&file); assert_eq!( json!(diagnostics.all()), json!([ { "range": { "start": { "line": 3, "character": 12, }, "end": { "line": 3, "character": 13, }, }, "severity": 1, "code": 2322, "source": "deno-ts", "message": "Type 'number' is not assignable to type 'string'.", }, ]), ); client.shutdown(); } #[test] fn lsp_diagnostics_warn_redirect() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( 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", }, }), ); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [ ["http://127.0.0.1:4545/x_deno_warning.js"], "file:///a/file.ts", ], }), ); let diagnostics = client.read_diagnostics(); assert_eq!( diagnostics.messages_with_source("deno"), lsp::PublishDiagnosticsParams { uri: lsp::Uri::from_str("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), } ); client.shutdown(); } #[test] fn lsp_redirect_quick_fix() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( 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", }, }), ); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [ ["http://127.0.0.1:4545/x_deno_warning.js"], "file:///a/file.ts", ], }), ); let diagnostics = client .read_diagnostics() .messages_with_source("deno") .diagnostics; let res = 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"] } })), ); assert_eq!( res, json!([{ "title": "Update specifier to its redirected specifier.", "kind": "quickfix", "diagnostics": [ { "range": { "start": { "line": 0, "character": 19 }, "end": { "line": 0, "character": 60 } }, "severity": 3, "code": "redirect", "source": "deno", "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\".", "data": { "specifier": "http://127.0.0.1:4545/x_deno_warning.js", "redirect": "http://127.0.0.1:4545/lsp/x_deno_warning_redirect.js" } } ], "edit": { "changes": { "file:///a/file.ts": [ { "range": { "start": { "line": 0, "character": 19 }, "end": { "line": 0, "character": 60 } }, "newText": "\"http://127.0.0.1:4545/lsp/x_deno_warning_redirect.js\"" } ] } } }]) ); client.shutdown(); } #[test] fn lsp_lockfile_redirect_resolution() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.write("deno.json", json!({}).to_string()); temp_dir.write("deno.lock", json!({ "version": "3", "redirects": { "http://localhost:4545/subdir/mod1.ts": "http://localhost:4545/subdir/mod2.ts", }, "remote": {}, }).to_string()); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import \"http://localhost:4545/subdir/mod1.ts\";\n", }, })); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], temp_dir.url().join("file.ts").unwrap()], }), ); client.read_diagnostics(); let res = client.write_request( "textDocument/definition", json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap() }, "position": { "line": 0, "character": 7 }, }), ); assert_eq!( res, json!([{ "targetUri": "deno:/http/localhost%3A4545/subdir/mod2.ts", "targetRange": { "start": { "line": 0, "character": 0 }, "end": { "line": 1, "character": 0 }, }, "targetSelectionRange": { "start": { "line": 0, "character": 0 }, "end": { "line": 1, "character": 0 }, }, }]), ); client.shutdown(); } // Regression test for https://github.com/denoland/vscode_deno/issues/1157. #[test] fn lsp_diagnostics_brackets_in_file_name() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": "file:///a/%5Bfile%5D.ts", "languageId": "typescript", "version": 1, "text": "/** @deprecated */\nexport const a = \"a\";\n\na;\n", }, })); assert_eq!( json!(diagnostics.all()), json!([ { "range": { "start": { "line": 3, "character": 0 }, "end": { "line": 3, "character": 1 }, }, "severity": 4, "code": 6385, "source": "deno-ts", "message": "'a' is deprecated.", "relatedInformation": [ { "location": { "uri": "file:///a/%5Bfile%5D.ts", "range": { "start": { "line": 0, "character": 4 }, "end": { "line": 0, "character": 16 }, }, }, "message": "The declaration was marked as deprecated here.", }, ], "tags": [2], }, ]), ); client.shutdown(); } #[test] fn lsp_diagnostics_deprecated() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "/** @deprecated */\nexport const a = \"a\";\n\na;\n", }, })); assert_eq!( json!(diagnostics.all_messages()), json!([{ "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": [ { "location": { "uri": "file:///a/file.ts", "range": { "start": { "line": 0, "character": 4, }, "end": { "line": 0, "character": 16, }, }, }, "message": "The declaration was marked as deprecated here.", }, ], "tags": [2] } ], "version": 1 }]) ); client.shutdown(); } #[test] fn lsp_diagnostics_deno_types() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client .did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "/// \n/// "#, ); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], file2.url()], }), ); let diagnostics = client.did_open_file(&file); assert_eq!(json!(diagnostics.all()), json!([])); } #[test] fn lsp_diagnostics_refresh_dependents() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file_00.ts", "languageId": "typescript", "version": 1, "text": "export const a = \"a\";\n", }, })); client.did_open(json!({ "textDocument": { "uri": "file:///a/file_01.ts", "languageId": "typescript", "version": 1, "text": "export * from \"./file_00.ts\";\n", }, })); let diagnostics = client.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 .messages_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 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" } ] }), ); let diagnostics = client.read_diagnostics(); assert_eq!(json!(diagnostics.all()), json!([])); // no diagnostics now client.shutdown(); } // Regression test for https://github.com/denoland/deno/issues/10897. #[test] fn lsp_ts_diagnostics_refresh_on_lsp_version_reset() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write("file.ts", r#"Deno.readTextFileSync(1);"#); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": temp_dir.read_to_string("file.ts"), }, })); assert_eq!(diagnostics.all().len(), 1); client.write_notification( "textDocument/didClose", json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), }, }), ); temp_dir.remove_file("file.ts"); // VSCode opens with `version: 1` again because the file was deleted. Ensure // diagnostics are still refreshed. client.did_open_raw(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "", }, })); temp_dir.write("file.ts", r#""#); client.did_save(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), }, })); let diagnostics = client.read_diagnostics(); assert_eq!(diagnostics.all(), vec![]); client.shutdown(); } #[test] fn lsp_diagnostics_none_for_resolving_types() { let context = TestContextBuilder::for_npm().use_temp_cwd().build(); context .temp_dir() .write("deno.json", r#"{ "unstable": ["byonm"] }"#); context.temp_dir().write( "package.json", r#"{ "dependencies": { "@denotest/monaco-editor": "*" } }"#, ); context.run_npm("install"); let mut client = context.new_lsp_command().build(); client.initialize_default(); // The types for this package will succeed, but the code will fail // because the package is only made for bundling and not meant to // run in Deno or Node. let diagnostics = client.did_open(json!({ "textDocument": { "uri": context.temp_dir().path().join("file.ts").url_file(), "languageId": "typescript", "version": 1, "text": concat!( "import * as a from \"@denotest/monaco-editor\";\n", "console.log(new a.Editor())\n", ) }, })); let diagnostics = diagnostics.all(); assert!(diagnostics.is_empty(), "{:?}", diagnostics); client.shutdown(); } #[test] fn lsp_jupyter_diagnostics() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": "deno-notebook-cell:/a/file.ts#abc", "languageId": "typescript", "version": 1, "text": "Deno.readTextFileSync(1234);", }, })); assert_eq!( json!(diagnostics.all_messages()), json!([ { "uri": "deno-notebook-cell:/a/file.ts#abc", "diagnostics": [ { "range": { "start": { "line": 0, "character": 22, }, "end": { "line": 0, "character": 26, }, }, "severity": 1, "code": 2345, "source": "deno-ts", "message": "Argument of type 'number' is not assignable to parameter of type 'string | URL'.", }, ], "version": 1, }, ]) ); client.shutdown(); } #[test] fn lsp_untitled_file_diagnostics() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": "untitled:///a/file.ts", "languageId": "typescript", "version": 1, "text": "Deno.readTextFileSync(1234);", }, })); assert_eq!( json!(diagnostics.all_messages()), json!([ { "uri": "untitled:///a/file.ts", "diagnostics": [ { "range": { "start": { "line": 0, "character": 22, }, "end": { "line": 0, "character": 26, }, }, "severity": 1, "code": 2345, "source": "deno-ts", "message": "Argument of type 'number' is not assignable to parameter of type 'string | URL'.", }, ], "version": 1, }, ]) ); client.shutdown(); } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PerformanceAverage { pub name: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct PerformanceAverages { averages: Vec, } #[test] fn lsp_performance() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "console.log(Deno.args);\n" } })); client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 0, "character": 19 } }), ); let res = client.write_request_with_res_as::( "deno/performance", json!(null), ); let mut averages = res .averages .iter() .map(|a| a.name.as_str()) .collect::>(); averages.sort(); assert_eq!( averages, vec![ "lsp.did_open", "lsp.hover", "lsp.initialize", "lsp.testing_update", "lsp.update_cache", "lsp.update_diagnostics_deps", "lsp.update_diagnostics_lint", "lsp.update_diagnostics_ts", "lsp.update_global_cache", "tsc.host.$getAssets", "tsc.host.$getDiagnostics", "tsc.host.$getSupportedCodeFixes", "tsc.host.getQuickInfoAtPosition", "tsc.op.op_is_node_file", "tsc.op.op_load", "tsc.op.op_script_names", "tsc.request.$getAssets", "tsc.request.$getSupportedCodeFixes", "tsc.request.getQuickInfoAtPosition", ] ); client.shutdown(); } #[test] fn lsp_format_no_changes() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "console;\n" } })); let res = client.write_request( "textDocument/formatting", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "options": { "tabSize": 2, "insertSpaces": true } }), ); assert_eq!(res, json!(null)); client.assert_no_notification("window/showMessage"); client.shutdown(); } #[test] fn lsp_format_error() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "console test test\n" } })); let res = client.write_request( "textDocument/formatting", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "options": { "tabSize": 2, "insertSpaces": true } }), ); assert_eq!(res, json!(null)); client.shutdown(); } #[test] fn lsp_format_mbc() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "const bar = '👍🇺🇸😃'\nconsole.log('hello deno')\n" } })); let res = client.write_request( "textDocument/formatting", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "options": { "tabSize": 2, "insertSpaces": true } }), ); assert_eq!( res, json!([{ "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 13 } }, "newText": "\"" }, { "range": { "start": { "line": 0, "character": 21 }, "end": { "line": 0, "character": 22 } }, "newText": "\";" }, { "range": { "start": { "line": 1, "character": 12 }, "end": { "line": 1, "character": 13 } }, "newText": "\"" }, { "range": { "start": { "line": 1, "character": 23 }, "end": { "line": 1, "character": 25 } }, "newText": "\");" }]) ); client.shutdown(); } #[test] fn lsp_format_exclude_with_config() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.fmt.jsonc", r#"{ "fmt": { "exclude": ["ignored.ts"], "options": { "useTabs": true, "lineWidth": 40, "indentWidth": 8, "singleQuote": true, "proseWrap": "always" } } }"#, ); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_config("./deno.fmt.jsonc"); }); let file_uri = temp_dir.url().join("ignored.ts").unwrap(); client.did_open(json!({ "textDocument": { "uri": file_uri, "languageId": "typescript", "version": 1, "text": "function myFunc(){}" } })); let res = client.write_request( "textDocument/formatting", json!({ "textDocument": { "uri": file_uri }, "options": { "tabSize": 2, "insertSpaces": true } }), ); assert_eq!(res, json!(null)); client.shutdown(); } #[test] fn lsp_format_exclude_default_config() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.fmt.jsonc", r#"{ "fmt": { "exclude": ["ignored.ts"], "options": { "useTabs": true, "lineWidth": 40, "indentWidth": 8, "singleQuote": true, "proseWrap": "always" } } }"#, ); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_config("./deno.fmt.jsonc"); }); let file_uri = temp_dir.url().join("ignored.ts").unwrap(); client.did_open(json!({ "textDocument": { "uri": file_uri, "languageId": "typescript", "version": 1, "text": "function myFunc(){}" } })); let res = client.write_request( "textDocument/formatting", json!({ "textDocument": { "uri": file_uri }, "options": { "tabSize": 2, "insertSpaces": true } }), ); assert_eq!(res, json!(null)); client.shutdown(); } #[test] fn lsp_format_json() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); let json_file = source_file(temp_dir.path().join("file.json"), "{\"key\":\"value\"}"); let mut client = context.new_lsp_command().build(); client.initialize_default(); let res = client.write_request( "textDocument/formatting", json!({ "textDocument": { "uri": json_file.url(), }, "options": { "tabSize": 2, "insertSpaces": true } }), ); assert_eq!( res, 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" } ]) ); client.shutdown(); } #[test] fn lsp_format_editor_options() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); let file = source_file( temp_dir.path().join("file.ts"), "if (true) {\n console.log();\n}\n", ); let mut client = context.new_lsp_command().build(); client.initialize_default(); let res = client.write_request( "textDocument/formatting", json!({ "textDocument": { "uri": file.url(), }, "options": { "tabSize": 4, "insertSpaces": true, }, }), ); assert_eq!( res, json!([ { "range": { "start": { "line": 1, "character": 0 }, "end": { "line": 1, "character": 0 }, }, "newText": " ", }, ]) ); let res = client.write_request( "textDocument/formatting", json!({ "textDocument": { "uri": file.url(), }, "options": { "tabSize": 2, "insertSpaces": false, }, }), ); assert_eq!( res, json!([ { "range": { "start": { "line": 1, "character": 0 }, "end": { "line": 1, "character": 2 }, }, "newText": "\t", }, ]) ); client.shutdown(); } #[test] fn lsp_json_no_diagnostics() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open_raw(json!({ "textDocument": { "uri": "file:///a/file.json", "languageId": "json", "version": 1, "text": "{\"key\":\"value\"}" } })); let res = client.write_request( "textDocument/semanticTokens/full", json!({ "textDocument": { "uri": "file:///a/file.json" } }), ); assert_eq!(res, json!(null)); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.json" }, "position": { "line": 0, "character": 3 } }), ); assert_eq!(res, json!(null)); client.shutdown(); } #[test] fn lsp_json_import_with_query_string() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write("data.json", r#"{"k": "v"}"#); temp_dir.write( "main.ts", r#" import data from "./data.json?1" with { type: "json" }; console.log(data); "#, ); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open_raw(json!({ "textDocument": { "uri": temp_dir.url().join("data.json").unwrap(), "languageId": "json", "version": 1, "text": temp_dir.read_to_string("data.json"), } })); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("main.ts").unwrap(), "languageId": "typescript", "version": 1, "text": temp_dir.read_to_string("main.ts"), } })); assert_eq!(diagnostics.all(), vec![]); client.shutdown(); } #[test] fn lsp_format_markdown() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); let markdown_file = source_file(temp_dir.path().join("file.md"), "# Hello World"); let mut client = context.new_lsp_command().build(); client.initialize_default(); let res = client.write_request( "textDocument/formatting", json!({ "textDocument": { "uri": markdown_file.url() }, "options": { "tabSize": 2, "insertSpaces": true } }), ); assert_eq!( res, 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" } ]) ); client.shutdown(); } #[test] fn lsp_format_html() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); let html_file = source_file(temp_dir.path().join("file.html"), " "); let mut client = context.new_lsp_command().build(); client.initialize_default(); let res = client.write_request( "textDocument/formatting", json!({ "textDocument": { "uri": html_file.url() }, "options": { "tabSize": 2, "insertSpaces": true, }, }), ); assert_eq!( res, json!([ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 2 }, }, "newText": "", }, { "range": { "start": { "line": 0, "character": 15 }, "end": { "line": 0, "character": 15 }, }, "newText": "\n", }, ]), ); client.shutdown(); } #[test] fn lsp_format_css() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); let css_file = source_file(temp_dir.path().join("file.css"), " foo {}"); let mut client = context.new_lsp_command().build(); client.initialize_default(); let res = client.write_request( "textDocument/formatting", json!({ "textDocument": { "uri": css_file.url() }, "options": { "tabSize": 2, "insertSpaces": true, }, }), ); assert_eq!( res, json!([ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 2 }, }, "newText": "", }, { "range": { "start": { "line": 0, "character": 8 }, "end": { "line": 0, "character": 8 }, }, "newText": "\n", }, ]), ); client.shutdown(); } #[test] fn lsp_format_yaml() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); let yaml_file = source_file(temp_dir.path().join("file.yaml"), " foo: 1"); let mut client = context.new_lsp_command().build(); client.initialize_default(); let res = client.write_request( "textDocument/formatting", json!({ "textDocument": { "uri": yaml_file.url() }, "options": { "tabSize": 2, "insertSpaces": true, }, }), ); assert_eq!( res, json!([ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 2 }, }, "newText": "", }, { "range": { "start": { "line": 0, "character": 8 }, "end": { "line": 0, "character": 8 }, }, "newText": "\n", }, ]), ); client.shutdown(); } #[test] fn lsp_format_with_config() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.fmt.jsonc", r#"{ "fmt": { "options": { "useTabs": true, "lineWidth": 40, "indentWidth": 8, "singleQuote": true, "proseWrap": "always" } } } "#, ); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_config("./deno.fmt.jsonc"); }); let ts_file = temp_dir.path().join("file.ts"); client .did_open( json!({ "textDocument": { "uri": ts_file.url_file(), "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}" } }), ); // The options below should be ignored in favor of configuration from config file. let res = client.write_request( "textDocument/formatting", json!({ "textDocument": { "uri": ts_file.url_file() }, "options": { "tabSize": 2, "insertSpaces": true, } }), ); assert_eq!( res, 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" }] ) ); client.shutdown(); } #[test] fn lsp_markdown_no_diagnostics() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open_raw(json!({ "textDocument": { "uri": "file:///a/file.md", "languageId": "markdown", "version": 1, "text": "# Hello World" } })); let res = client.write_request( "textDocument/semanticTokens/full", json!({ "textDocument": { "uri": "file:///a/file.md" } }), ); assert_eq!(res, json!(null)); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.md" }, "position": { "line": 0, "character": 3 } }), ); assert_eq!(res, json!(null)); client.shutdown(); } #[test] fn lsp_configuration_did_change() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "import * as a from \"http://localhost:4545/x/a@\"" } })); client.change_configuration(json!({ "deno": { "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": [], } })); let list = client.get_completion_list( "file:///a/file.ts", (0, 46), json!({ "triggerKind": 2, "triggerCharacter": "@" }), ); assert!(!list.is_incomplete); assert_eq!(list.items.len(), 3); let res = client.write_request( "completionItem/resolve", json!({ "label": "v2.0.0", "kind": 19, "detail": "(version)", "sortText": "0000000003", "filterText": "http://localhost:4545/x/a@v2.0.0", "textEdit": { "range": { "start": { "line": 0, "character": 20 }, "end": { "line": 0, "character": 46 } }, "newText": "http://localhost:4545/x/a@v2.0.0" } }), ); assert_eq!( res, json!({ "label": "v2.0.0", "kind": 19, "detail": "(version)", "sortText": "0000000003", "filterText": "http://localhost:4545/x/a@v2.0.0", "textEdit": { "range": { "start": { "line": 0, "character": 20 }, "end": { "line": 0, "character": 46 } }, "newText": "http://localhost:4545/x/a@v2.0.0" } }) ); client.shutdown(); } #[test] fn lsp_completions_complete_function_calls() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "[]." } })); client.change_configuration(json!({ "deno": { "enable": true, }, "typescript": { "suggest": { "completeFunctionCalls": true, }, }, })); let list = client.get_completion_list( "file:///a/file.ts", (0, 3), json!({ "triggerKind": 2, "triggerCharacter": ".", }), ); assert!(!list.is_incomplete); let res = client.write_request( "completionItem/resolve", json!({ "label": "map", "kind": 2, "sortText": "1", "insertTextFormat": 1, "data": { "tsc": { "specifier": "file:///a/file.ts", "position": 3, "name": "map", "useCodeSnippet": true } } }), ); assert_eq!( res, json!({ "label": "map", "kind": 2, "detail": "(method) Array.map(callbackfn: (value: never, index: number, array: never[]) => U, thisArg?: any): U[]", "documentation": { "kind": "markdown", "value": "Calls a defined callback function on each element of an array, and returns an array that contains the results.\n\n*@param* - callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array. \n\n*@param* - thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value." }, "sortText": "1", "insertText": "map(${1:callbackfn})", "insertTextFormat": 2, }) ); client.shutdown(); } #[test] fn lsp_workspace_symbol() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "export class A {\n fieldA: string;\n fieldB: string;\n}\n", } })); client.did_open(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 mut res = client.write_request( "workspace/symbol", json!({ "query": "field" }), ); // Replace `range` fields with `null` values. These are not important // for assertion and require to be updated if we change unstable APIs. for obj in res.as_array_mut().unwrap().iter_mut() { *obj .as_object_mut() .unwrap() .get_mut("location") .unwrap() .as_object_mut() .unwrap() .get_mut("range") .unwrap() = Value::Null; } assert_eq!( res, json!([ { "name": "fieldA", "kind": 8, "location": { "uri": "file:///a/file.ts", "range": null, }, "containerName": "A" }, { "name": "fieldB", "kind": 8, "location": { "uri": "file:///a/file.ts", "range": null, }, "containerName": "A" }, { "name": "fieldC", "kind": 8, "location": { "uri": "file:///a/file_01.ts", "range": null, }, "containerName": "B" }, { "name": "fieldD", "kind": 8, "location": { "uri": "file:///a/file_01.ts", "range": null, }, "containerName": "B" }, { "name": "ClassFieldDecoratorContext", "kind": 11, "location": { "uri": "deno:/asset/lib.decorators.d.ts", "range": null, }, "containerName": "" }, ]) ); client.shutdown(); } #[test] fn lsp_code_actions_ignore_lint() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "let message = 'Hello, Deno!';\nconsole.log(message);\n" } })); let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "range": { "start": { "line": 1, "character": 5 }, "end": { "line": 1, "character": 12 } }, "context": { "diagnostics": [ { "range": { "start": { "line": 1, "character": 5 }, "end": { "line": 1, "character": 12 } }, "severity": 1, "code": "prefer-const", "source": "deno-lint", "message": "'message' is never reassigned\nUse 'const' instead", "relatedInformation": [] } ], "only": ["quickfix"] } }), ); assert_eq!( res, json!([{ "title": "Disable prefer-const for this line", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 1, "character": 5 }, "end": { "line": 1, "character": 12 } }, "severity": 1, "code": "prefer-const", "source": "deno-lint", "message": "'message' is never reassigned\nUse 'const' instead", "relatedInformation": [] }], "edit": { "changes": { "file:///a/file.ts": [{ "range": { "start": { "line": 1, "character": 0 }, "end": { "line": 1, "character": 0 } }, "newText": "// deno-lint-ignore prefer-const\n" }] } } }, { "title": "Disable prefer-const for the entire file", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 1, "character": 5 }, "end": { "line": 1, "character": 12 } }, "severity": 1, "code": "prefer-const", "source": "deno-lint", "message": "'message' is never reassigned\nUse 'const' instead", "relatedInformation": [] }], "edit": { "changes": { "file:///a/file.ts": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "// deno-lint-ignore-file prefer-const\n" }] } } }, { "title": "Ignore lint errors for the entire file", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 1, "character": 5 }, "end": { "line": 1, "character": 12 } }, "severity": 1, "code": "prefer-const", "source": "deno-lint", "message": "'message' is never reassigned\nUse 'const' instead", "relatedInformation": [] }], "edit": { "changes": { "file:///a/file.ts": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "// deno-lint-ignore-file\n" }] } } }]) ); client.shutdown(); } /// This test exercises updating an existing deno-lint-ignore-file comment. #[test] fn lsp_code_actions_update_ignore_lint() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(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 res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "range": { "start": { "line": 3, "character": 5 }, "end": { "line": 3, "character": 15 } }, "context": { "diagnostics": [{ "range": { "start": { "line": 3, "character": 5 }, "end": { "line": 3, "character": 15 } }, "severity": 1, "code": "prefer-const", "source": "deno-lint", "message": "'snake_case' is never reassigned\nUse 'const' instead", "relatedInformation": [] }], "only": ["quickfix"] } }), ); assert_eq!( res, json!([{ "title": "Disable prefer-const for this line", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 3, "character": 5 }, "end": { "line": 3, "character": 15 } }, "severity": 1, "code": "prefer-const", "source": "deno-lint", "message": "'snake_case' is never reassigned\nUse 'const' instead", "relatedInformation": [] }], "edit": { "changes": { "file:///a/file.ts": [{ "range": { "start": { "line": 3, "character": 0 }, "end": { "line": 3, "character": 0 } }, "newText": "// deno-lint-ignore prefer-const\n" }] } } }, { "title": "Disable prefer-const for the entire file", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 3, "character": 5 }, "end": { "line": 3, "character": 15 } }, "severity": 1, "code": "prefer-const", "source": "deno-lint", "message": "'snake_case' is never reassigned\nUse 'const' instead", "relatedInformation": [] }], "edit": { "changes": { "file:///a/file.ts": [{ "range": { "start": { "line": 1, "character": 34 }, "end": { "line": 1, "character": 34 } }, "newText": " prefer-const" }] } } }, { "title": "Ignore lint errors for the entire file", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 3, "character": 5 }, "end": { "line": 3, "character": 15 } }, "severity": 1, "code": "prefer-const", "source": "deno-lint", "message": "'snake_case' is never reassigned\nUse 'const' instead", "relatedInformation": [] }], "edit": { "changes": { "file:///a/file.ts": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "// deno-lint-ignore-file\n" }] } } }]) ); client.shutdown(); } #[test] fn lsp_code_actions_lint_fixes() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "window;", } })); let diagnostics = diagnostics.all(); let diagnostic = &diagnostics[0]; let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 6 } }, "context": { "diagnostics": [diagnostic], "only": ["quickfix"] } }), ); assert_eq!( res, json!([{ "title": "Rename window to globalThis", "kind": "quickfix", "diagnostics": [diagnostic], "edit": { "changes": { "file:///a/file.ts": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 6 } }, "newText": "globalThis" }] } } }, { "title": "Disable no-window for this line", "kind": "quickfix", "diagnostics": [diagnostic], "edit": { "changes": { "file:///a/file.ts": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "// deno-lint-ignore no-window\n" }] } } }, { "title": "Disable no-window for the entire file", "kind": "quickfix", "diagnostics": [diagnostic], "edit": { "changes": { "file:///a/file.ts": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "// deno-lint-ignore-file no-window\n" }] } } }, { "title": "Ignore lint errors for the entire file", "kind": "quickfix", "diagnostics": [diagnostic], "edit": { "changes": { "file:///a/file.ts": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "// deno-lint-ignore-file\n" }] } } }]) ); client.shutdown(); } #[test] fn lsp_lint_with_config() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.lint.jsonc", r#"{ "lint": { "rules": { "exclude": ["camelcase"], "include": ["ban-untagged-todo"], "tags": [] } } } "#, ); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_config("./deno.lint.jsonc"); }); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "// TODO: fixme\nexport async function non_camel_case() {\nconsole.log(\"finished!\")\n}" } })); let diagnostics = diagnostics.all(); assert_eq!(diagnostics.len(), 1); assert_eq!( diagnostics[0].code, Some(lsp::NumberOrString::String("ban-untagged-todo".to_string())) ); client.shutdown(); } #[test] fn lsp_lint_exclude_with_config() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.lint.jsonc", r#"{ "lint": { "exclude": ["ignored.ts"], "rules": { "exclude": ["camelcase"], "include": ["ban-untagged-todo"], "tags": [] } } }"#, ); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_config("./deno.lint.jsonc"); }); let diagnostics = client.did_open( 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.all(); assert_eq!(diagnostics, Vec::new()); client.shutdown(); } #[test] fn lsp_jsx_import_source_pragma() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(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 ; } ", } })); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [ ["http://127.0.0.1:4545/jsx/jsx-runtime"], "file:///a/file.tsx", ], }), ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.tsx" }, "position": { "line": 0, "character": 25 } }), ); assert_eq!( res, 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 } } }) ); let diagnostics = client.read_diagnostics(); println!("{:?}", diagnostics); client.shutdown(); } #[ignore = "https://github.com/denoland/deno/issues/21770"] #[test] fn lsp_jsx_import_source_config_file_automatic_cache() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.json", json!({ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "http://localhost:4545/jsx", }, }) .to_string(), ); let mut client = context.new_lsp_command().build(); client.initialize_default(); let mut diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.tsx").unwrap(), "languageId": "typescriptreact", "version": 1, "text": " export function Foo() { return
; } ", }, })); // The caching is done on an asynchronous task spawned after init, so there's // a chance it wasn't done in time and we need to wait for another batch of // diagnostics. let mut version = 1; while !diagnostics.all().is_empty() { std::thread::sleep(std::time::Duration::from_millis(50)); // The post-cache diagnostics update triggers inconsistently on CI for some // reason. Force it with this notification. version += 1; client.write_notification( "textDocument/didChange", json!({ "textDocument": { "uri": temp_dir.url().join("file.tsx").unwrap(), "version": version, }, "contentChanges": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, }, "text": "", }, ], }), ); diagnostics = client.read_diagnostics(); } assert_eq!(diagnostics.all(), vec![]); client.shutdown(); } #[ignore = "https://github.com/denoland/deno/issues/21770"] #[test] fn lsp_jsx_import_source_package_json_automatic_cache() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.json", json!({ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact", }, "nodeModulesDir": false, }) .to_string(), ); temp_dir.write( "package.json", json!({ "dependencies": { "preact": "^10.19.6", }, }) .to_string(), ); let mut client = context.new_lsp_command().build(); client.initialize_default(); let mut diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.tsx").unwrap(), "languageId": "typescriptreact", "version": 1, "text": " export function Foo() { return
; } ", }, })); // The caching is done on an asynchronous task spawned after init, so there's // a chance it wasn't done in time and we need to wait for another batch of // diagnostics. let mut version = 1; while !diagnostics.all().is_empty() { std::thread::sleep(std::time::Duration::from_millis(50)); // The post-cache diagnostics update triggers inconsistently on CI for some // reason. Force it with this notification. version += 1; client.write_notification( "textDocument/didChange", json!({ "textDocument": { "uri": temp_dir.url().join("file.tsx").unwrap(), "version": version, }, "contentChanges": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, }, "text": "", }, ], }), ); diagnostics = client.read_diagnostics(); } assert_eq!(json!(diagnostics.all()), json!([])); client.shutdown(); } #[test] fn lsp_jsx_import_source_byonm_preact() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .add_npm_env_vars() .build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.json", json!({ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "npm:preact@^10.19.6" }, "unstable": ["byonm"], }) .to_string(), ); temp_dir.write( "package.json", json!({ "dependencies": { "preact": "^10.19.6", }, }) .to_string(), ); let file = source_file(temp_dir.path().join("file.tsx"), r#"
;"#); context.run_npm("install"); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open_file(&file); assert_eq!(json!(diagnostics.all()), json!([])); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": file.url() }, "position": { "line": 0, "character": 1 }, }), ); assert_eq!( res, json!({ "contents": [ { "language": "typescript", "value": "(property) JSXInternal.IntrinsicElements.div: JSXInternal.HTMLAttributes", }, "", ], "range": { "start": { "line": 0, "character": 1 }, "end": { "line": 0, "character": 4 }, }, }), ); client.shutdown(); } #[test] fn lsp_jsx_import_source_types_pragma() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.tsx", "languageId": "typescriptreact", "version": 1, "text": "/** @jsxImportSource http://localhost:4545/jsx */ /** @jsxImportSourceTypes http://localhost:4545/jsx-types */ /** @jsxRuntime automatic */ function A() { return Hello; } export function B() { return ; } ", } })); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [ [], "file:///a/file.tsx", ], }), ); let diagnostics = client.read_diagnostics(); assert_eq!(json!(diagnostics.all()), json!([])); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.tsx" }, "position": { "line": 0, "character": 25 } }), ); assert_eq!( res, json!({ "contents": { "kind": "markdown", "value": "**Resolved Dependency**\n\n**Code**: http​://localhost:4545/jsx/jsx-runtime\n\n**Types**: http​://localhost:4545/jsx-types/jsx-runtime\n", }, "range": { "start": { "line": 0, "character": 21 }, "end": { "line": 0, "character": 46 } } }) ); client.shutdown(); } #[derive(Debug, Clone, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] struct TestData { id: String, label: String, steps: Option>, range: Option, } #[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, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct EnqueuedTestModule { text_document: lsp::TextDocumentIdentifier, ids: Vec, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct TestRunResponseParams { enqueued: Vec, } #[test] fn lsp_testing_api() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); let contents = r#" Deno.test({ name: "test a", async fn(t) { console.log("test a"); await t.step("step of test a", () => {}); const kv = await Deno.openKv(); kv.close(); } }); "#; temp_dir.write("./test.ts", contents); temp_dir.write("./deno.jsonc", "{}"); let specifier = temp_dir.url().join("test.ts").unwrap(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.change_configuration(json!({ "deno": { "enable": true, "unstable": ["kv"], }, })); client.did_open(json!({ "textDocument": { "uri": specifier, "languageId": "typescript", "version": 1, "text": contents, } })); let notification = client.read_notification_with_method::("deno/testModule"); let params: TestModuleNotificationParams = serde_json::from_value(notification.unwrap()).unwrap(); assert_eq!(params.text_document.uri.as_str(), specifier.as_str()); 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_eq!( test.range, Some(lsp::Range { start: lsp::Position { line: 1, character: 5, }, end: lsp::Position { line: 1, character: 9, } }) ); let steps = test.steps.as_ref().unwrap(); assert_eq!(steps.len(), 1); let step = &steps[0]; assert_eq!(step.label, "step of test a"); assert_eq!( step.range, Some(lsp::Range { start: lsp::Position { line: 5, character: 12, }, end: lsp::Position { line: 5, character: 16, } }) ); let res = client.write_request_with_res_as::( "deno/testRun", json!({ "id": 1, "kind": "run", }), ); assert_eq!(res.enqueued.len(), 1); assert_eq!( res.enqueued[0].text_document.uri.as_str(), specifier.as_str() ); assert_eq!(res.enqueued[0].ids.len(), 1); let id = res.enqueued[0].ids[0].clone(); let notification = client.read_notification_with_method::("deno/testRunProgress"); assert_eq!( notification, Some(json!({ "id": 1, "message": { "type": "started", "test": { "textDocument": { "uri": specifier, }, "id": id, }, } })) ); let notification = client.read_notification_with_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. Occasionally 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 notification = client.read_notification_with_method::("deno/testRunProgress"); assert_eq!( notification, Some(json!({ "id": 1, "message": { "type": "started", "test": { "textDocument": { "uri": specifier, }, "id": id, "stepId": step.id, }, } })) ); let notification = client.read_notification_with_method::("deno/testRunProgress"); let mut notification = notification.unwrap(); let duration = notification .as_object_mut() .unwrap() .get_mut("message") .unwrap() .as_object_mut() .unwrap() .remove("duration"); assert!(duration.is_some()); assert_eq!( notification, json!({ "id": 1, "message": { "type": "passed", "test": { "textDocument": { "uri": specifier, }, "id": id, "stepId": step.id, }, } }) ); let notification = client.read_notification_with_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 notification = client.read_notification_with_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)), } // Regression test for https://github.com/denoland/vscode_deno/issues/899. temp_dir.write("./test.ts", ""); client.write_notification( "textDocument/didChange", json!({ "textDocument": { "uri": temp_dir.url().join("test.ts").unwrap(), "version": 2 }, "contentChanges": [{ "text": "" }], }), ); assert_eq!(client.read_diagnostics().all().len(), 0); let notification = client.read_notification_with_method::("deno/testModuleDelete"); assert_eq!( notification, Some(json!({ "textDocument": { "uri": temp_dir.url().join("test.ts").unwrap() } })) ); client.shutdown(); } #[test] fn lsp_closed_file_find_references() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write("./mod.ts", "export const a = 5;"); temp_dir.write( "./mod.test.ts", "import { a } from './mod.ts'; console.log(a);", ); let temp_dir_url = temp_dir.url(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir_url.join("mod.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#"export const a = 5;"# } })); let res = client.write_request( "textDocument/references", json!({ "textDocument": { "uri": temp_dir_url.join("mod.ts").unwrap(), }, "position": { "line": 0, "character": 13 }, "context": { "includeDeclaration": false } }), ); assert_eq!( res, json!([{ "uri": temp_dir_url.join("mod.test.ts").unwrap(), "range": { "start": { "line": 0, "character": 9 }, "end": { "line": 0, "character": 10 } } }, { "uri": temp_dir_url.join("mod.test.ts").unwrap(), "range": { "start": { "line": 0, "character": 42 }, "end": { "line": 0, "character": 43 } } }]) ); client.shutdown(); } #[test] fn lsp_closed_file_find_references_low_document_pre_load() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("sub_dir"); temp_dir.write("./other_file.ts", "export const b = 5;"); temp_dir.write("./sub_dir/mod.ts", "export const a = 5;"); temp_dir.write( "./sub_dir/mod.test.ts", "import { a } from './mod.ts'; console.log(a);", ); let temp_dir_url = temp_dir.url(); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_preload_limit(1); }); client.did_open(json!({ "textDocument": { "uri": temp_dir_url.join("sub_dir/mod.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#"export const a = 5;"# } })); let res = client.write_request( "textDocument/references", json!({ "textDocument": { "uri": temp_dir_url.join("sub_dir/mod.ts").unwrap(), }, "position": { "line": 0, "character": 13 }, "context": { "includeDeclaration": false } }), ); // won't have results because the document won't be pre-loaded assert_eq!(res, json!([])); client.shutdown(); } #[test] fn lsp_closed_file_find_references_excluded_path() { // we exclude any files or folders in the "exclude" part of // the config file from being pre-loaded let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("sub_dir"); temp_dir.create_dir_all("other_dir/sub_dir"); temp_dir.write("./sub_dir/mod.ts", "export const a = 5;"); temp_dir.write( "./sub_dir/mod.test.ts", "import { a } from './mod.ts'; console.log(a);", ); temp_dir.write( "./other_dir/sub_dir/mod.test.ts", "import { a } from '../../sub_dir/mod.ts'; console.log(a);", ); temp_dir.write( "deno.json", r#"{ "exclude": [ "./sub_dir/mod.test.ts", "./other_dir/sub_dir", ] }"#, ); let temp_dir_url = temp_dir.url(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir_url.join("sub_dir/mod.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#"export const a = 5;"# } })); let res = client.write_request( "textDocument/references", json!({ "textDocument": { "uri": temp_dir_url.join("sub_dir/mod.ts").unwrap(), }, "position": { "line": 0, "character": 13 }, "context": { "includeDeclaration": false } }), ); // won't have results because the documents won't be pre-loaded assert_eq!(res, json!([])); client.shutdown(); } #[test] fn lsp_data_urls_with_jsx_compiler_option() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.json", r#"{ "compilerOptions": { "jsx": "react-jsx" } }"#, ); let mut client = context.new_lsp_command().build(); client.initialize_default(); let uri = temp_dir.url().join("main.ts").unwrap(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": uri, "languageId": "typescript", "version": 1, "text": "import a from \"data:application/typescript,export default 5;\";\na;" } })).all(); assert_eq!(diagnostics.len(), 0); let res: Value = client.write_request( "textDocument/references", json!({ "textDocument": { "uri": uri }, "position": { "line": 1, "character": 1 }, "context": { "includeDeclaration": false } }), ); assert_eq!( res, json!([{ "uri": uri, "range": { "start": { "line": 0, "character": 7 }, "end": { "line": 0, "character": 8 } } }, { "uri": uri, "range": { "start": { "line": 1, "character": 0 }, "end": { "line": 1, "character": 1 } } }, { "uri": "deno:/ed0224c51f7e2a845dfc0941ed6959675e5e3e3d2a39b127f0ff569c1ffda8d8/data_url.ts", "range": { "start": { "line": 0, "character": 7 }, "end": {"line": 0, "character": 14 }, }, }]) ); client.shutdown(); } #[test] fn lsp_node_modules_dir() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.json", json!({ "nodeModulesDir": "none", }) .to_string(), ); // having a package.json should have no effect on whether // a node_modules dir is created temp_dir.write("package.json", "{}"); let mut client = context.new_lsp_command().build(); client.initialize_default(); let file_uri = temp_dir.url().join("file.ts").unwrap(); client.did_open(json!({ "textDocument": { "uri": file_uri, "languageId": "typescript", "version": 1, "text": "import chalk from 'npm:chalk';\nimport path from 'node:path';\n\nconsole.log(chalk.green(path.join('a', 'b')));", } })); let cache = |client: &mut LspClient| { client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [["npm:chalk", "npm:@types/node"], file_uri], }), ); }; cache(&mut client); assert!(!temp_dir.path().join("node_modules").exists()); // a lockfile will be created here because someone did an explicit cache let lockfile_path = temp_dir.path().join("deno.lock"); assert!(lockfile_path.exists()); lockfile_path.remove_file(); temp_dir.write( temp_dir.path().join("deno.json"), "{ \"nodeModulesDir\": \"auto\", \"lock\": false }\n", ); let refresh_config = |client: &mut LspClient| { client.change_configuration(json!({ "deno": { "enable": true, "config": "./deno.json", "codeLens": { "implementations": true, "references": true, }, "importMap": null, "lint": false, "suggest": { "autoImports": true, "completeFunctionCalls": false, "names": true, "paths": true, "imports": {}, }, "unstable": [], } })); }; refresh_config(&mut client); let diagnostics = client.read_diagnostics(); assert_eq!(diagnostics.all().len(), 2, "{:#?}", diagnostics); // not cached cache(&mut client); assert!(temp_dir.path().join("node_modules/chalk").exists()); assert!(temp_dir.path().join("node_modules/@types/node").exists()); assert!(!lockfile_path.exists()); // was disabled // now add a lockfile and cache temp_dir.write( temp_dir.path().join("deno.json"), "{ \"nodeModulesDir\": \"auto\" }\n", ); refresh_config(&mut client); cache(&mut client); let diagnostics = client.read_diagnostics(); assert_eq!(diagnostics.all().len(), 0, "{:#?}", diagnostics); assert!(lockfile_path.exists()); // the declaration should be found in the node_modules directory let res = client.write_request( "textDocument/references", json!({ "textDocument": { "uri": file_uri, }, "position": { "line": 0, "character": 7 }, // chalk "context": { "includeDeclaration": false } }), ); // ensure that it's using the node_modules directory let references = res.as_array().unwrap(); assert_eq!(references.len(), 2, "references: {:#?}", references); let uri = references[1] .as_object() .unwrap() .get("uri") .unwrap() .as_str() .unwrap(); // canonicalize for mac let path = temp_dir.path().join("node_modules").canonicalize(); assert_starts_with!( uri, ModuleSpecifier::from_file_path(&path).unwrap().as_str() ); client.shutdown(); } #[test] fn lsp_vendor_dir() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let local_file_uri = temp_dir.url().join("file.ts").unwrap(); client.did_open(json!({ "textDocument": { "uri": local_file_uri, "languageId": "typescript", "version": 1, "text": "import { returnsHi } from 'http://localhost:4545/subdir/mod1.ts';\nconst test: string = returnsHi();\nconsole.log(test);", } })); let cache = |client: &mut LspClient| { client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [["http://localhost:4545/subdir/mod1.ts"], local_file_uri], }), ); }; cache(&mut client); assert!(!temp_dir.path().join("vendor").exists()); // read the diagnostic update after caching let diagnostics = client.read_diagnostics(); assert_eq!(diagnostics.all().len(), 0); temp_dir.write( temp_dir.path().join("deno.json"), "{ \"vendor\": true, \"lock\": false }\n", ); client.change_configuration(json!({ "deno": { "enable": true, "config": "./deno.json", "codeLens": { "implementations": true, "references": true, }, "importMap": null, "lint": false, "suggest": { "autoImports": true, "completeFunctionCalls": false, "names": true, "paths": true, "imports": {}, }, "unstable": [], } })); let diagnostics = client.read_diagnostics(); // won't be cached until a manual cache occurs assert_eq!( diagnostics .all() .iter() .map(|d| d.message.as_str()) .collect::>(), vec![ "Uncached or missing remote URL: http://localhost:4545/subdir/mod1.ts" ] ); assert!(!temp_dir .path() .join("vendor/http_localhost_4545/subdir/mod1.ts") .exists()); // now cache cache(&mut client); let diagnostics = client.read_diagnostics(); assert_eq!(diagnostics.all().len(), 0, "{:#?}", diagnostics); // cached assert!(temp_dir .path() .join("vendor/http_localhost_4545/subdir/mod1.ts") .exists()); // the declaration should be found in the vendor directory let res = client.write_request( "textDocument/references", json!({ "textDocument": { "uri": local_file_uri, }, "position": { "line": 0, "character": 9 }, // returnsHi "context": { "includeDeclaration": false } }), ); // ensure that it's using the vendor directory let references = res.as_array().unwrap(); assert_eq!(references.len(), 2, "references: {:#?}", references); let uri = references[1] .as_object() .unwrap() .get("uri") .unwrap() .as_str() .unwrap(); let file_path = temp_dir .path() .join("vendor/http_localhost_4545/subdir/mod1.ts"); let remote_file_uri = file_path.url_file(); assert_eq!(uri, remote_file_uri.as_str()); let file_text = file_path.read_to_string(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": remote_file_uri, "languageId": "typescript", "version": 1, "text": file_text, } })); assert_eq!(diagnostics.all(), Vec::new()); client.write_notification( "textDocument/didChange", json!({ "textDocument": { "uri": remote_file_uri, "version": 2 }, "contentChanges": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 17, "character": 0 }, }, "text": "export function returnsHi(): number { return new Date(); }" } ] }), ); let diagnostics = client.read_diagnostics(); assert_eq!( json!( diagnostics .messages_with_file_and_source(remote_file_uri.as_str(), "deno-ts") .diagnostics ), json!([ { "range": { "start": { "line": 0, "character": 38 }, "end": { "line": 0, "character": 44 } }, "severity": 1, "code": 2322, "source": "deno-ts", "message": "Type 'Date' is not assignable to type 'number'." } ]), ); assert_eq!( json!( diagnostics .messages_with_file_and_source(local_file_uri.as_str(), "deno-ts") .diagnostics ), json!([ { "range": { "start": { "line": 1, "character": 6 }, "end": { "line": 1, "character": 10 } }, "severity": 1, "code": 2322, "source": "deno-ts", "message": "Type 'number' is not assignable to type 'string'." } ]), ); assert_eq!(diagnostics.all().len(), 2); // now try doing a relative import into the vendor directory client.write_notification( "textDocument/didChange", json!({ "textDocument": { "uri": local_file_uri, "version": 2 }, "contentChanges": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 2, "character": 0 }, }, "text": "import { returnsHi } from './vendor/subdir/mod1.ts';\nconst test: string = returnsHi();\nconsole.log(test);" } ] }), ); let diagnostics = client.read_diagnostics(); assert_eq!( json!( diagnostics .messages_with_file_and_source(local_file_uri.as_str(), "deno") .diagnostics ), json!([ { "range": { "start": { "line": 0, "character": 26 }, "end": { "line": 0, "character": 51 } }, "severity": 1, "code": "resolver-error", "source": "deno", "message": "Importing from the vendor directory is not permitted. Use a remote specifier instead or disable vendoring." } ]), ); client.shutdown(); } #[test] fn lsp_deno_json_scopes_import_map() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("project1"); temp_dir.create_dir_all("project2/project3"); temp_dir.write( "project1/deno.json", json!({ "imports": { "foo": "./foo1.ts", }, }) .to_string(), ); temp_dir.write("project1/foo1.ts", ""); temp_dir.write( "project2/deno.json", json!({ "imports": { "foo": "./foo2.ts", }, }) .to_string(), ); temp_dir.write("project2/foo2.ts", ""); temp_dir.write( "project2/project3/deno.json", json!({ "imports": { "foo": "./foo3.ts", }, }) .to_string(), ); temp_dir.write("project2/project3/foo3.ts", ""); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project1/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import \"foo\";\n", }, })); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": temp_dir.url().join("project1/file.ts").unwrap(), }, "position": { "line": 0, "character": 7 }, }), ); assert_eq!( res, json!({ "contents": { "kind": "markdown", "value": format!("**Resolved Dependency**\n\n**Code**: file​{}\n", temp_dir.url().join("project1/foo1.ts").unwrap().as_str().trim_start_matches("file")), }, "range": { "start": { "line": 0, "character": 7 }, "end": { "line": 0, "character": 12 }, }, }) ); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project2/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import \"foo\";\n", }, })); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": temp_dir.url().join("project2/file.ts").unwrap(), }, "position": { "line": 0, "character": 7 }, }), ); assert_eq!( res, json!({ "contents": { "kind": "markdown", "value": format!("**Resolved Dependency**\n\n**Code**: file​{}\n", temp_dir.url().join("project2/foo2.ts").unwrap().as_str().trim_start_matches("file")), }, "range": { "start": { "line": 0, "character": 7 }, "end": { "line": 0, "character": 12 }, }, }) ); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project2/project3/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import \"foo\";\n", }, })); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": temp_dir.url().join("project2/project3/file.ts").unwrap(), }, "position": { "line": 0, "character": 7 }, }), ); assert_eq!( res, json!({ "contents": { "kind": "markdown", "value": format!("**Resolved Dependency**\n\n**Code**: file​{}\n", temp_dir.url().join("project2/project3/foo3.ts").unwrap().as_str().trim_start_matches("file")), }, "range": { "start": { "line": 0, "character": 7 }, "end": { "line": 0, "character": 12 }, }, }) ); client.shutdown(); } #[test] fn lsp_deno_json_scopes_vendor_dir() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("project1"); temp_dir.create_dir_all("project2/project3"); temp_dir.write( "project1/deno.json", json!({ "vendor": true, }) .to_string(), ); temp_dir.write( "project2/deno.json", json!({ "vendor": true, }) .to_string(), ); temp_dir.write( "project2/project3/deno.json", json!({ "vendor": true, }) .to_string(), ); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project1/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import \"http://localhost:4545/subdir/mod1.ts\";\n", }, })); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], temp_dir.url().join("project1/file.ts").unwrap()], }), ); let res = client.write_request( "textDocument/definition", json!({ "textDocument": { "uri": temp_dir.url().join("project1/file.ts").unwrap(), }, "position": { "line": 0, "character": 7 }, }), ); assert_eq!( res, json!([{ "targetUri": temp_dir.url().join("project1/vendor/http_localhost_4545/subdir/mod1.ts").unwrap(), "targetRange": { "start": { "line": 0, "character": 0, }, "end": { "line": 17, "character": 0, }, }, "targetSelectionRange": { "start": { "line": 0, "character": 0, }, "end": { "line": 17, "character": 0, }, }, }]), ); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project2/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import \"http://localhost:4545/subdir/mod2.ts\";\n", }, })); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], temp_dir.url().join("project2/file.ts").unwrap()], }), ); let res = client.write_request( "textDocument/definition", json!({ "textDocument": { "uri": temp_dir.url().join("project2/file.ts").unwrap(), }, "position": { "line": 0, "character": 7 }, }), ); assert_eq!( res, json!([{ "targetUri": temp_dir.url().join("project2/vendor/http_localhost_4545/subdir/mod2.ts").unwrap(), "targetRange": { "start": { "line": 0, "character": 0, }, "end": { "line": 1, "character": 0, }, }, "targetSelectionRange": { "start": { "line": 0, "character": 0, }, "end": { "line": 1, "character": 0, }, }, }]), ); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project2/project3/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import \"http://localhost:4545/subdir/mod3.js\";\n", }, })); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], temp_dir.url().join("project2/project3/file.ts").unwrap()], }), ); let res = client.write_request( "textDocument/definition", json!({ "textDocument": { "uri": temp_dir.url().join("project2/project3/file.ts").unwrap(), }, "position": { "line": 0, "character": 7 }, }), ); assert_eq!( res, json!([{ "targetUri": temp_dir.url().join("project2/project3/vendor/http_localhost_4545/subdir/mod3.js").unwrap(), "targetRange": { "start": { "line": 0, "character": 0, }, "end": { "line": 1, "character": 0, }, }, "targetSelectionRange": { "start": { "line": 0, "character": 0, }, "end": { "line": 1, "character": 0, }, }, }]), ); client.shutdown(); } #[test] fn lsp_deno_json_scopes_node_modules_dir() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("project1"); temp_dir.create_dir_all("project2/project3"); temp_dir.write( "project1/deno.json", json!({ "nodeModulesDir": "auto", }) .to_string(), ); temp_dir.write( "project2/deno.json", json!({ "nodeModulesDir": "auto", }) .to_string(), ); temp_dir.write( "project2/project3/deno.json", json!({ "nodeModulesDir": "auto", }) .to_string(), ); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project1/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import \"npm:@denotest/add@1\";\n", }, })); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], temp_dir.url().join("project1/file.ts").unwrap()], }), ); let res = client.write_request( "textDocument/definition", json!({ "textDocument": { "uri": temp_dir.url().join("project1/file.ts").unwrap(), }, "position": { "line": 0, "character": 7 }, }), ); // The temp dir is symlinked in macos, and `node_modules` is canonicalized. let canon_temp_dir = Url::from_directory_path(temp_dir.path().canonicalize()).unwrap(); assert_eq!( res, json!([{ "targetUri": canon_temp_dir.join("project1/node_modules/.deno/@denotest+add@1.0.0/node_modules/@denotest/add/index.d.ts").unwrap(), "targetRange": { "start": { "line": 0, "character": 0, }, "end": { "line": 1, "character": 0, }, }, "targetSelectionRange": { "start": { "line": 0, "character": 0, }, "end": { "line": 1, "character": 0, }, }, }]), ); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project2/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import \"npm:@denotest/add@1\";\n", }, })); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], temp_dir.url().join("project2/file.ts").unwrap()], }), ); let res = client.write_request( "textDocument/definition", json!({ "textDocument": { "uri": temp_dir.url().join("project2/file.ts").unwrap(), }, "position": { "line": 0, "character": 7 }, }), ); assert_eq!( res, json!([{ "targetUri": canon_temp_dir.join("project2/node_modules/.deno/@denotest+add@1.0.0/node_modules/@denotest/add/index.d.ts").unwrap(), "targetRange": { "start": { "line": 0, "character": 0, }, "end": { "line": 1, "character": 0, }, }, "targetSelectionRange": { "start": { "line": 0, "character": 0, }, "end": { "line": 1, "character": 0, }, }, }]), ); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project2/project3/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import \"npm:@denotest/add@1\";\n", }, })); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], temp_dir.url().join("project2/project3/file.ts").unwrap()], }), ); let res = client.write_request( "textDocument/definition", json!({ "textDocument": { "uri": temp_dir.url().join("project2/project3/file.ts").unwrap(), }, "position": { "line": 0, "character": 7 }, }), ); assert_eq!( res, json!([{ "targetUri": canon_temp_dir.join("project2/project3/node_modules/.deno/@denotest+add@1.0.0/node_modules/@denotest/add/index.d.ts").unwrap(), "targetRange": { "start": { "line": 0, "character": 0, }, "end": { "line": 1, "character": 0, }, }, "targetSelectionRange": { "start": { "line": 0, "character": 0, }, "end": { "line": 1, "character": 0, }, }, }]), ); client.shutdown(); } #[test] fn lsp_deno_json_scopes_ts_config() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("project1"); temp_dir.create_dir_all("project2"); temp_dir.write("project1/deno.json", json!({}).to_string()); temp_dir.write( "project2/deno.json", json!({ "compilerOptions": { "lib": ["deno.worker"], }, }) .to_string(), ); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project1/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "Window;\nWorkerGlobalScope;\n", }, })); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project2/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "Window;\nWorkerGlobalScope;\n", }, })); assert_eq!( json!(diagnostics.all_messages()), json!([ { "uri": temp_dir.url().join("project2/file.ts").unwrap(), "version": 1, "diagnostics": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 6 }, }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'Window'.", }, ], }, { "uri": temp_dir.url().join("project1/file.ts").unwrap(), "version": 1, "diagnostics": [ { "range": { "start": { "line": 1, "character": 0 }, "end": { "line": 1, "character": 17 }, }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'WorkerGlobalScope'.", }, ], } ]), ); client.shutdown(); } #[test] fn lsp_deno_json_scopes_declaration_files() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("project1"); temp_dir.create_dir_all("project2"); temp_dir.write("project1/deno.json", json!({}).to_string()); temp_dir.write("project2/deno.json", json!({}).to_string()); temp_dir.write("project1/foo.d.ts", "declare type Foo = number;\n"); temp_dir.write("project2/bar.d.ts", "declare type Bar = number;\n"); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project1/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "export const foo: Foo = 1;\nexport const bar: Bar = 1;\n", }, })); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project2/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "export const foo: Foo = 1;\nexport const bar: Bar = 1;\n", }, })); assert_eq!( json!(diagnostics.all_messages()), json!([ { "uri": temp_dir.url().join("project2/file.ts").unwrap(), "version": 1, "diagnostics": [ { "range": { "start": { "line": 0, "character": 18 }, "end": { "line": 0, "character": 21 }, }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'Foo'.", }, ], }, { "uri": temp_dir.url().join("project1/file.ts").unwrap(), "version": 1, "diagnostics": [ { "range": { "start": { "line": 1, "character": 18 }, "end": { "line": 1, "character": 21 }, }, "severity": 1, "code": 2304, "source": "deno-ts", "message": "Cannot find name 'Bar'.", }, ], } ]), ); client.shutdown(); } #[test] fn lsp_deno_json_scopes_find_references() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("project1"); temp_dir.create_dir_all("project2"); temp_dir.write("project1/deno.json", json!({}).to_string()); temp_dir.write("project2/deno.json", json!({}).to_string()); let file1 = source_file( temp_dir.path().join("project1/file.ts"), "export const foo = 1;\n", ); let file2 = source_file( temp_dir.path().join("project2/file.ts"), "export { foo } from \"../project1/file.ts\";\n", ); let mut client = context.new_lsp_command().build(); client.initialize_default(); let res = client.write_request( "textDocument/references", json!({ "textDocument": file1.identifier(), "position": file1.range_of("foo").start, "context": { "includeDeclaration": true, }, }), ); assert_eq!( res, json!([ { "uri": file1.url(), "range": file1.range_of("foo"), }, { "uri": file2.url(), "range": file2.range_of("foo"), }, ]), ); client.shutdown(); } #[test] fn lsp_deno_json_scopes_file_rename_import_edits() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("project1"); temp_dir.create_dir_all("project2"); temp_dir.write("project1/deno.json", json!({}).to_string()); temp_dir.write("project2/deno.json", json!({}).to_string()); let file1 = source_file(temp_dir.path().join("project1/file.ts"), ""); let file2 = source_file( temp_dir.path().join("project2/file.ts"), "import \"../project1/file.ts\";\n", ); let mut client = context.new_lsp_command().build(); client.initialize_default(); let res = client.write_request( "workspace/willRenameFiles", json!({ "files": [ { "oldUri": file1.url(), "newUri": file1.url().join("file_renamed.ts").unwrap(), }, ], }), ); assert_eq!( res, json!({ "documentChanges": [ { "textDocument": { "uri": file2.url(), "version": null, }, "edits": [ { "range": file2.range_of("../project1/file.ts"), "newText": "../project1/file_renamed.ts", }, ], }, ], }), ); client.shutdown(); } #[test] fn lsp_deno_json_scopes_goto_implementations() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("project1"); temp_dir.create_dir_all("project2"); temp_dir.write("project1/deno.json", json!({}).to_string()); temp_dir.write("project2/deno.json", json!({}).to_string()); let file1 = source_file( temp_dir.path().join("project1/file.ts"), "export interface Foo {}\n", ); let file2 = source_file( temp_dir.path().join("project2/file.ts"), r#" import type { Foo } from "../project1/file.ts"; export class SomeFoo implements Foo {} "#, ); let mut client = context.new_lsp_command().build(); client.initialize_default(); let res = client.write_request( "textDocument/implementation", json!({ "textDocument": file1.identifier(), "position": file1.range_of("Foo").start, }), ); assert_eq!( res, json!([ { "targetUri": file2.url(), "targetRange": file2.range_of("export class SomeFoo implements Foo {}"), "targetSelectionRange": file2.range_of("SomeFoo"), }, ]), ); client.shutdown(); } #[test] fn lsp_deno_json_scopes_call_hierarchy() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("project1"); temp_dir.create_dir_all("project2"); temp_dir.create_dir_all("project3"); temp_dir.write("project1/deno.json", json!({}).to_string()); temp_dir.write("project2/deno.json", json!({}).to_string()); temp_dir.write("project3/deno.json", json!({}).to_string()); let file1 = source_file( temp_dir.path().join("project1/file.ts"), r#" export function foo() {} "#, ); let file2 = source_file( temp_dir.path().join("project2/file.ts"), r#" import { foo } from "../project1/file.ts"; export function bar() { foo(); } "#, ); let file3 = source_file( temp_dir.path().join("project3/file.ts"), r#" import { bar } from "../project2/file.ts"; bar(); "#, ); let mut client = context.new_lsp_command().build(); client.initialize_default(); let res = client.write_request( "textDocument/prepareCallHierarchy", json!({ "textDocument": file2.identifier(), "position": file2.range_of("bar").start, }), ); assert_eq!( &res, &json!([ { "name": "bar", "kind": 12, "detail": "", "uri": file2.url(), "range": { "start": { "line": 2, "character": 6 }, "end": { "line": 4, "character": 7 }, }, "selectionRange": file2.range_of("bar"), }, ]), ); let item = res.as_array().unwrap().first().unwrap(); let res = client .write_request("callHierarchy/incomingCalls", json!({ "item": item })); assert_eq!( res, json!([ { "from": { "name": "file.ts", "kind": 2, "detail": "project3", "uri": file3.url(), "range": { "start": { "line": 1, "character": 6 }, "end": { "line": 3, "character": 4 }, }, "selectionRange": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, }, }, "fromRanges": [ { "start": { "line": 2, "character": 6 }, "end": { "line": 2, "character": 9 }, }, ], }, ]), ); let res = client .write_request("callHierarchy/outgoingCalls", json!({ "item": item })); assert_eq!( res, json!([ { "to": { "name": "foo", "kind": 12, "detail": "", "uri": file1.url(), "range": file1.range_of("export function foo() {}"), "selectionRange": file1.range_of("foo"), }, "fromRanges": [ { "start": { "line": 3, "character": 8 }, "end": { "line": 3, "character": 11 }, }, ], }, ]), ); client.shutdown(); } #[test] fn lsp_deno_json_scopes_rename_symbol() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("project1"); temp_dir.create_dir_all("project2"); temp_dir.write("project1/deno.json", json!({}).to_string()); temp_dir.write("project2/deno.json", json!({}).to_string()); let file1 = source_file( temp_dir.path().join("project1/file.ts"), "export const foo = 1;\n", ); let file2 = source_file( temp_dir.path().join("project2/file.ts"), "export { foo } from \"../project1/file.ts\";\n", ); let mut client = context.new_lsp_command().build(); client.initialize_default(); let res = client.write_request( "textDocument/rename", json!({ "textDocument": file1.identifier(), "position": file1.range_of("foo").start, "newName": "bar", }), ); assert_eq!( res, json!({ "documentChanges": [ { "textDocument": { "uri": file1.url(), "version": null, }, "edits": [ { "range": file1.range_of("foo"), "newText": "bar", }, ], }, { "textDocument": { "uri": file2.url(), "version": null, }, "edits": [ { "range": file2.range_of("foo"), "newText": "bar", }, ], }, ], }), ); client.shutdown(); } #[test] fn lsp_deno_json_scopes_search_symbol() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("project1"); temp_dir.create_dir_all("project2"); temp_dir.write("project1/deno.json", json!({}).to_string()); temp_dir.write("project2/deno.json", json!({}).to_string()); let file1 = source_file( temp_dir.path().join("project1/file.ts"), "export const someSymbol1 = 1;\n", ); let file2 = source_file( temp_dir.path().join("project2/file.ts"), "export const someSymbol2 = 2;\n", ); let mut client = context.new_lsp_command().build(); client.initialize_default(); let res = client.write_request("workspace/symbol", json!({ "query": "someSymbol" })); assert_eq!( res, json!([ { "name": "someSymbol1", "kind": 13, "location": { "uri": file1.url(), "range": file1.range_of("someSymbol1 = 1"), }, "containerName": "", }, { "name": "someSymbol2", "kind": 13, "location": { "uri": file2.url(), "range": file2.range_of("someSymbol2 = 2"), }, "containerName": "", }, ]), ); client.shutdown(); } #[test] fn lsp_deno_json_workspace_fmt_config() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.json", json!({ "workspace": ["project1", "project2"], "fmt": { "semiColons": false, }, }) .to_string(), ); temp_dir.create_dir_all("project1"); temp_dir.write( "project1/deno.json", json!({ "fmt": { "singleQuote": true, }, }) .to_string(), ); temp_dir.create_dir_all("project2"); temp_dir.write("project2/deno.json", json!({}).to_string()); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "console.log(\"\");\n", }, })); let res = client.write_request( "textDocument/formatting", json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), }, "options": { "tabSize": 2, "insertSpaces": true, }, }), ); assert_eq!( res, json!([{ "range": { "start": { "line": 0, "character": 15 }, "end": { "line": 0, "character": 16 }, }, "newText": "", }]) ); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project1/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "console.log(\"\");\n", }, })); let res = client.write_request( "textDocument/formatting", json!({ "textDocument": { "uri": temp_dir.url().join("project1/file.ts").unwrap(), }, "options": { "tabSize": 2, "insertSpaces": true, }, }), ); assert_eq!( res, json!([{ "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 16 }, }, "newText": "'')", }]) ); // `project2/file.ts` should use the fmt settings from `deno.json`, since it // has no fmt field. client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project2/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "console.log(\"\");\n", }, })); let res = client.write_request( "textDocument/formatting", json!({ "textDocument": { "uri": temp_dir.url().join("project2/file.ts").unwrap(), }, "options": { "tabSize": 2, "insertSpaces": true, }, }), ); assert_eq!( res, json!([{ "range": { "start": { "line": 0, "character": 15 }, "end": { "line": 0, "character": 16 }, }, "newText": "", }]) ); client.shutdown(); } #[test] fn lsp_deno_json_workspace_lint_config() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.json", json!({ "workspace": ["project1", "project2"], "lint": { "rules": { "include": ["camelcase"], }, }, }) .to_string(), ); temp_dir.create_dir_all("project1"); temp_dir.write( "project1/deno.json", json!({ "lint": { "rules": { "include": ["ban-untagged-todo"], }, }, }) .to_string(), ); temp_dir.create_dir_all("project2"); temp_dir.write("project2/deno.json", json!({}).to_string()); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#" // TODO: Unused var const snake_case_var = 1; console.log(snake_case_var); "#, }, })); assert_eq!( json!(diagnostics.messages_with_source("deno-lint")), json!({ "uri": temp_dir.url().join("file.ts").unwrap(), "diagnostics": [{ "range": { "start": { "line": 2, "character": 14 }, "end": { "line": 2, "character": 28 }, }, "severity": 2, "code": "camelcase", "source": "deno-lint", "message": "Identifier 'snake_case_var' is not in camel case.\nConsider renaming `snake_case_var` to `snakeCaseVar`", }], "version": 1, }) ); client.write_notification( "textDocument/didClose", json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), }, }), ); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project1/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#" // TODO: Unused var const snake_case_var = 1; console.log(snake_case_var); "#, }, })); assert_eq!( json!(diagnostics.messages_with_source("deno-lint")), json!({ "uri": temp_dir.url().join("project1/file.ts").unwrap(), "diagnostics": [{ "range": { "start": { "line": 1, "character": 8 }, "end": { "line": 1, "character": 27 }, }, "severity": 2, "code": "ban-untagged-todo", "source": "deno-lint", "message": "TODO should be tagged with (@username) or (#issue)\nAdd a user tag or issue reference to the TODO comment, e.g. TODO(@djones), TODO(djones), TODO(#123)", }, { "range": { "start": { "line": 2, "character": 14 }, "end": { "line": 2, "character": 28 }, }, "severity": 2, "code": "camelcase", "source": "deno-lint", "message": "Identifier 'snake_case_var' is not in camel case.\nConsider renaming `snake_case_var` to `snakeCaseVar`", }], "version": 1, }) ); client.write_notification( "textDocument/didClose", json!({ "textDocument": { "uri": temp_dir.url().join("project1/file.ts").unwrap(), }, }), ); // `project2/file.ts` should use the lint settings from `deno.json`, since it // has no lint field. let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project2/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#" // TODO: Unused var const snake_case_var = 1; console.log(snake_case_var); "#, }, })); assert_eq!( json!(diagnostics.messages_with_source("deno-lint")), json!({ "uri": temp_dir.url().join("project2/file.ts").unwrap(), "diagnostics": [{ "range": { "start": { "line": 2, "character": 14 }, "end": { "line": 2, "character": 28 }, }, "severity": 2, "code": "camelcase", "source": "deno-lint", "message": "Identifier 'snake_case_var' is not in camel case.\nConsider renaming `snake_case_var` to `snakeCaseVar`", }], "version": 1, }) ); client.shutdown(); } #[test] fn lsp_deno_json_workspace_import_map() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("project1/project2"); temp_dir.write( "project1/deno.json", json!({ "workspace": ["project2"], "imports": { "foo": "./foo1.ts", }, }) .to_string(), ); temp_dir.write("project1/foo1.ts", ""); temp_dir.write( "project1/project2/deno.json", json!({ // should overwrite the "foo" entry in the parent for this scope "imports": { "foo": "./foo2.ts", }, }) .to_string(), ); temp_dir.write("project1/project2/foo2.ts", ""); let mut client = context.new_lsp_command().build(); client.initialize_default(); // project1 resolution { client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project1/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import \"foo\";\n", }, })); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": temp_dir.url().join("project1/file.ts").unwrap(), }, "position": { "line": 0, "character": 7 }, }), ); assert_eq!( res, json!({ "contents": { "kind": "markdown", "value": format!("**Resolved Dependency**\n\n**Code**: file​{}\n", temp_dir.url().join("project1/foo1.ts").unwrap().as_str().trim_start_matches("file")), }, "range": { "start": { "line": 0, "character": 7 }, "end": { "line": 0, "character": 12 }, }, }) ); } // project1/project2 resolution { client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project1/project2/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import \"foo\";\n", }, })); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": temp_dir.url().join("project1/project2/file.ts").unwrap(), }, "position": { "line": 0, "character": 7 }, }), ); assert_eq!( res, json!({ "contents": { "kind": "markdown", "value": format!("**Resolved Dependency**\n\n**Code**: file​{}\n", temp_dir.url().join("project1/project2/foo2.ts").unwrap().as_str().trim_start_matches("file")), }, "range": { "start": { "line": 0, "character": 7 }, "end": { "line": 0, "character": 12 }, }, }) ); } client.shutdown(); } #[test] fn lsp_workspace_lockfile() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("project1/project2"); temp_dir.write( "project1/deno.json", json!({ "workspace": ["project2"], }) .to_string(), ); temp_dir.write("project1/deno.lock", json!({ "version": "3", "redirects": { "http://localhost:4545/subdir/mod1.ts": "http://localhost:4545/subdir/mod2.ts", }, "remote": {}, }).to_string()); temp_dir.write("project1/project2/deno.json", json!({}).to_string()); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project1/project2/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import \"http://localhost:4545/subdir/mod1.ts\";\n", }, })); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], temp_dir.url().join("project1/project2/file.ts").unwrap()], }), ); client.read_diagnostics(); let res = client.write_request( "textDocument/definition", json!({ "textDocument": { "uri": temp_dir.url().join("project1/project2/file.ts").unwrap() }, "position": { "line": 0, "character": 7 }, }), ); assert_eq!( res, json!([{ "targetUri": "deno:/http/localhost%3A4545/subdir/mod2.ts", "targetRange": { "start": { "line": 0, "character": 0 }, "end": { "line": 1, "character": 0 }, }, "targetSelectionRange": { "start": { "line": 0, "character": 0 }, "end": { "line": 1, "character": 0 }, }, }]), ); client.shutdown(); } #[test] fn lsp_deno_json_workspace_vendor_dir() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("project1/project2"); temp_dir.write( "project1/deno.json", json!({ "workspace": ["project2"], "vendor": true, }) .to_string(), ); temp_dir.write("project1/project2/deno.json", json!({}).to_string()); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project1/project2/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import \"http://localhost:4545/subdir/mod1.ts\";\n", }, })); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], temp_dir.url().join("project1/project2/file.ts").unwrap()], }), ); let res = client.write_request( "textDocument/definition", json!({ "textDocument": { "uri": temp_dir.url().join("project1/project2/file.ts").unwrap(), }, "position": { "line": 0, "character": 7 }, }), ); assert_eq!( res, json!([{ "targetUri": temp_dir.url().join("project1/vendor/http_localhost_4545/subdir/mod1.ts").unwrap(), "targetRange": { "start": { "line": 0, "character": 0, }, "end": { "line": 17, "character": 0, }, }, "targetSelectionRange": { "start": { "line": 0, "character": 0, }, "end": { "line": 17, "character": 0, }, }, }]), ); client.shutdown(); } #[test] fn lsp_deno_json_workspace_node_modules_dir() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.create_dir_all("project1/project2"); temp_dir.write( "project1/deno.json", json!({ "workspace": ["project2"], "nodeModulesDir": "auto", }) .to_string(), ); temp_dir.write("project1/project2/deno.json", json!({}).to_string()); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("project1/project2/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import \"npm:@denotest/add@1\";\n", }, })); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], temp_dir.url().join("project1/project2/file.ts").unwrap()], }), ); let res = client.write_request( "textDocument/definition", json!({ "textDocument": { "uri": temp_dir.url().join("project1/project2/file.ts").unwrap(), }, "position": { "line": 0, "character": 7 }, }), ); // The temp dir is symlinked in macos, and `node_modules` is canonicalized. let canon_temp_dir = Url::from_directory_path(temp_dir.path().canonicalize()).unwrap(); assert_eq!( res, json!([{ "targetUri": canon_temp_dir.join("project1/node_modules/.deno/@denotest+add@1.0.0/node_modules/@denotest/add/index.d.ts").unwrap(), "targetRange": { "start": { "line": 0, "character": 0, }, "end": { "line": 1, "character": 0, }, }, "targetSelectionRange": { "start": { "line": 0, "character": 0, }, "end": { "line": 1, "character": 0, }, }, }]), ); client.shutdown(); } #[test] fn lsp_deno_json_workspace_jsr_resolution() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.json", json!({ "workspace": ["project1"], }) .to_string(), ); temp_dir.create_dir_all("project1"); temp_dir.write( "project1/deno.json", json!({ "name": "@org/project1", "version": "1.0.0", "exports": { ".": "./mod.ts", }, }) .to_string(), ); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import \"jsr:@org/project1@^1.0.0\";\n", }, })); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), }, "position": { "line": 0, "character": 7 }, }), ); assert_eq!( res, json!({ "contents": { "kind": "markdown", "value": format!( "**Resolved Dependency**\n\n**Code**: file​://{}\n", temp_dir.url().join("project1/mod.ts").unwrap().path(), ), }, "range": { "start": { "line": 0, "character": 7 }, "end": { "line": 0, "character": 33 }, }, }), ); client.shutdown(); } #[test] fn lsp_npm_workspace() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); // TODO(nayeemrmn): Goto definition for local npm package imports should work // even with byonm. Remove this when fixed. temp_dir.write( "deno.json", json!({ "nodeModulesDir": "auto", }) .to_string(), ); temp_dir.write( "package.json", json!({ "workspaces": ["packages/*"] }) .to_string(), ); { temp_dir.create_dir_all("packages/add"); temp_dir.write( "packages/add/package.json", json!({ "name": "add", "version": "1.0.0", "exports": "./index.ts" }) .to_string(), ); temp_dir.write( "packages/add/index.ts", "export function add(a: number, b: number): number { return a + b; }", ); } { temp_dir.create_dir_all("packages/subtract"); temp_dir.write( "packages/subtract/package.json", json!({ "name": "add", "version": "1.0.0", "exports": "./index.ts", "dependencies": { "add": "^1.0.0" } }) .to_string(), ); } let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("packages/subtract/index.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import { add } from 'add';\nexport function subtract(a: number, b: number): number { return add(a, -b); }", }, })); assert_eq!(json!(diagnostics.all()), json!([])); let res = client.write_request( "textDocument/definition", json!({ "textDocument": { "uri": temp_dir.url().join("packages/subtract/index.ts").unwrap(), }, "position": { "line": 0, "character": 9 }, }), ); // The temp dir is symlinked on the CI assert_eq!( res, json!([{ "targetUri": temp_dir.url().join("packages/add/index.ts").unwrap(), "targetRange": { "start": { "line": 0, "character": 0, }, "end": { "line": 0, "character": 67, }, }, "targetSelectionRange": { "start": { "line": 0, "character": 16, }, "end": { "line": 0, "character": 19, }, }, }]), ); client.shutdown(); } #[test] fn lsp_import_unstable_bare_node_builtins_auto_discovered() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); let contents = r#"import path from "path";"#; temp_dir.write("main.ts", contents); temp_dir.write("deno.json", r#"{ "unstable": ["bare-node-builtins"] }"#); let main_script = temp_dir.url().join("main.ts").unwrap(); let mut client = context.new_lsp_command().capture_stderr().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": main_script, "languageId": "typescript", "version": 1, "text": contents, } })); let diagnostics = diagnostics .messages_with_file_and_source(main_script.as_ref(), "deno") .diagnostics .into_iter() .filter(|d| { d.code == Some(lsp::NumberOrString::String( "import-node-prefix-missing".to_string(), )) }) .collect::>(); // get the quick fixes let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": main_script }, "range": { "start": { "line": 0, "character": 16 }, "end": { "line": 0, "character": 18 }, }, "context": { "diagnostics": json!(diagnostics), "only": ["quickfix"] } }), ); assert_eq!( res, json!([{ "title": "Update specifier to node:path", "kind": "quickfix", "diagnostics": [ { "range": { "start": { "line": 0, "character": 17 }, "end": { "line": 0, "character": 23 } }, "severity": 2, "code": "import-node-prefix-missing", "source": "deno", "message": "\"path\" is resolved to \"node:path\". If you want to use a built-in Node module, add a \"node:\" prefix.", "data": { "specifier": "path" }, } ], "edit": { "changes": { main_script: [ { "range": { "start": { "line": 0, "character": 17 }, "end": { "line": 0, "character": 23 } }, "newText": "\"node:path\"" } ] } } }]) ); client.shutdown(); } #[test] fn lsp_jupyter_byonm_diagnostics() { let context = TestContextBuilder::for_npm().use_temp_cwd().build(); let temp_dir = context.temp_dir().path(); temp_dir.join("package.json").write_json(&json!({ "dependencies": { "@denotest/esm-basic": "*" } })); temp_dir.join("deno.json").write_json(&json!({ "unstable": ["byonm"] })); context.run_npm("install"); let mut client = context.new_lsp_command().build(); client.initialize_default(); let notebook_specifier = temp_dir.join("notebook.ipynb").url_file(); let notebook_specifier = format!( "{}#abc", notebook_specifier .to_string() .replace("file://", "deno-notebook-cell:") ); let diagnostics = client.did_open(json!({ "textDocument": { "uri": notebook_specifier, "languageId": "typescript", "version": 1, "text": "import { getValue, nonExistent } from '@denotest/esm-basic';\n console.log(getValue, nonExistent);", }, })); assert_eq!( json!(diagnostics.all_messages()), json!([ { "uri": notebook_specifier, "diagnostics": [ { "range": { "start": { "line": 0, "character": 19, }, "end": { "line": 0, "character": 30, }, }, "severity": 1, "code": 2305, "source": "deno-ts", "message": "Module '\"@denotest/esm-basic\"' has no exported member 'nonExistent'.", }, ], "version": 1, }, ]) ); client.shutdown(); } #[test] fn lsp_byonm() { let context = TestContextBuilder::for_npm().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.path().join("package.json").write_json(&json!({ "dependencies": { "@denotest/esm-basic": "*", }, })); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#" import "npm:chalk"; import "@denotest/esm-basic"; "#, }, })); assert_eq!( json!(diagnostics.all()), json!([ { "range": { "start": { "line": 1, "character": 15, }, "end": { "line": 1, "character": 26, }, }, "severity": 1, "code": "resolver-error", "source": "deno", "message": "Could not find a matching package for 'npm:chalk' in the node_modules directory. Ensure you have all your JSR and npm dependencies listed in your deno.json or package.json, then run `deno install`. Alternatively, turn on auto-install by specifying `\"nodeModulesDir\": \"auto\"` in your deno.json file.", }, { "range": { "start": { "line": 2, "character": 15, }, "end": { "line": 2, "character": 36, }, }, "severity": 1, "code": "resolver-error", "source": "deno", "message": "Could not resolve \"@denotest/esm-basic\", but found it in a package.json. Deno expects the node_modules/ directory to be up to date. Did you forget to run `deno install`?", }, ]) ); context.run_npm("install"); client.did_change_watched_files(json!({ "changes": [{ "uri": temp_dir.url().join("node_modules/.package-lock.json").unwrap(), "type": 1, }], })); let diagnostics = client.read_diagnostics(); assert_eq!( json!(diagnostics.all()), json!([ { "range": { "start": { "line": 1, "character": 15, }, "end": { "line": 1, "character": 26, }, }, "severity": 1, "code": "resolver-error", "source": "deno", "message": "Could not find a matching package for 'npm:chalk' in the node_modules directory. Ensure you have all your JSR and npm dependencies listed in your deno.json or package.json, then run `deno install`. Alternatively, turn on auto-install by specifying `\"nodeModulesDir\": \"auto\"` in your deno.json file.", }, ]) ); client.shutdown(); } #[test] fn lsp_sloppy_imports() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); let temp_dir = temp_dir.path(); temp_dir .join("deno.json") .write(r#"{ "unstable": ["sloppy-imports"] }"#); // for sloppy imports, the file must exist on the file system // to be resolved correctly temp_dir.join("a.ts").write("export class A {}"); temp_dir.join("b.ts").write("export class B {}"); temp_dir.join("c.js").write("export class C {}"); temp_dir.join("c.d.ts").write("export class C {}"); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_root_uri(temp_dir.url_dir()); }); client.did_open(json!({ "textDocument": { "uri": temp_dir.join("b.ts").url_file(), "languageId": "typescript", "version": 1, "text": "export class B {}", }, })); client.did_open(json!({ "textDocument": { "uri": temp_dir.join("c.js").url_file(), "languageId": "typescript", "version": 1, "text": "export class C {}", }, })); client.did_open(json!({ "textDocument": { "uri": temp_dir.join("c.d.ts").url_file(), "languageId": "typescript", "version": 1, "text": "export class C {}", }, })); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.join("file.ts").url_file(), "languageId": "typescript", "version": 1, "text": concat!( "import * as a from './a';\n", "import * as b from './b.js';\n", // this one's types resolve to a .d.ts file and we don't // bother warning about it because it's a bit complicated // to explain to use @deno-types in a diagnostic "import * as c from './c.js';\n", "console.log(a)\n", "console.log(b);\n", "console.log(c);\n", ), }, })); assert_eq!( json!(diagnostics.all()), json!([{ "range": { "start": { "line": 0, "character": 19 }, "end": { "line": 0, "character": 24 } }, "severity": 2, "code": "no-sloppy-imports", "source": "deno-lint", "message": "Sloppy imports are not allowed.", "data": [{ "description": "Add a '.ts' extension.", "changes": [{ "range": { "start": { "line": 0, "character": 19 }, "end": { "line": 0, "character": 24 }, }, "new_text": "'./a.ts'" }] }] }, { "range": { "start": { "line": 1, "character": 19 }, "end": { "line": 1, "character": 27 } }, "severity": 2, "code": "no-sloppy-imports", "source": "deno-lint", "message": "Sloppy imports are not allowed.", "data": [{ "description": "Change the extension to '.ts'.", "changes": [{ "range": { "start": { "line": 1, "character": 19 }, "end": { "line": 1, "character": 27 }, }, "new_text": "'./b.ts'" }] }] }, { "range": { "start": { "line": 2, "character": 19 }, "end": { "line": 2, "character": 27 } }, "severity": 2, "code": "no-sloppy-imports", "source": "deno-lint", "message": "Sloppy imports are not allowed.", "data": [{ "description": "Change the extension to '.d.ts'.", "changes": [{ "range": { "start": { "line": 2, "character": 19 }, "end": { "line": 2, "character": 27 }, }, "new_text": "'./c.d.ts'" }] }] }]) ); client.shutdown(); } #[test] fn lsp_sloppy_imports_prefers_dts() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write("deno.json", json!({}).to_string()); temp_dir.write("a.js", "export const foo: number;"); let a_dts = source_file(temp_dir.path().join("a.d.ts"), "export const foo = 3;"); let file = source_file( temp_dir.path().join("file.ts"), "import { foo } from './a.js';\nconsole.log(foo);", ); let mut client: LspClient = context.new_lsp_command().build(); client.initialize_default(); client.change_configuration(json!({ "deno": { "enable": true, "unstable": ["sloppy-imports"], }, })); let diagnostics = client.did_open_file(&file); // no other warnings because "a.js" exists assert_eq!( json!(diagnostics.all()), json!([{ "range": { "start": { "line": 0, "character": 20 }, "end": { "line": 0, "character": 28 } }, "severity": 2, "code": "no-sloppy-imports", "source": "deno-lint", "message": "Sloppy imports are not allowed.", "data": [{ "description": "Change the extension to '.d.ts'.", "changes": [{ "range": { "start": { "line": 0, "character": 20 }, "end": { "line": 0, "character": 28 }, }, "new_text": "'./a.d.ts'" }] }] }]) ); let diagnostics = client.did_open_file(&a_dts); assert_eq!(json!(diagnostics.for_file(&a_dts.url())), json!([])); let response = client.write_request( "textDocument/references", json!({ "textDocument": a_dts.identifier(), "position": a_dts.range_of("foo").start, "context": { "includeDeclaration": false } }), ); assert_json_subset( response, json!([ { "uri": file.url(), // the import "range": file.range_of("foo"), }, { "uri": file.url(), // the usage "range": file.range_of_nth(1, "foo"), } ]), ); } #[test] fn sloppy_imports_not_enabled() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); let temp_dir = temp_dir.path(); temp_dir.join("deno.json").write(r#"{}"#); // The enhanced, more helpful error message is only available // when the file exists on the file system at the moment because // it's a little more complicated to hook it up otherwise. temp_dir.join("a.ts").write("export class A {}"); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_root_uri(temp_dir.url_dir()); }); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.join("file.ts").url_file(), "languageId": "typescript", "version": 1, "text": "import * as a from './a';\nconsole.log(a)\n", }, })); assert_eq!( diagnostics.messages_with_source("deno"), lsp::PublishDiagnosticsParams { uri: temp_dir.join("file.ts").uri_file(), diagnostics: vec![lsp::Diagnostic { range: lsp::Range { start: lsp::Position { line: 0, character: 19 }, end: lsp::Position { line: 0, character: 24 } }, severity: Some(lsp::DiagnosticSeverity::ERROR), code: Some(lsp::NumberOrString::String("no-local".to_string())), source: Some("deno".to_string()), message: format!( "Unable to load a local module: {}\nMaybe add a '.ts' extension.", temp_dir.join("a").url_file(), ), data: Some(json!({ "specifier": temp_dir.join("a").url_file(), "to": temp_dir.join("a.ts").url_file(), "message": "Add a '.ts' extension.", })), ..Default::default() }], version: Some(1), } ); let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": temp_dir.join("file.ts").url_file() }, "range": { "start": { "line": 0, "character": 19 }, "end": { "line": 0, "character": 24 } }, "context": { "diagnostics": [{ "range": { "start": { "line": 0, "character": 19 }, "end": { "line": 0, "character": 24 } }, "severity": 3, "code": "no-local", "source": "deno", "message": format!( "Unable to load a local module: {}\nMaybe add a '.ts' extension.", temp_dir.join("a").url_file(), ), "data": { "specifier": temp_dir.join("a").url_file(), "to": temp_dir.join("a.ts").url_file(), "message": "Add a '.ts' extension.", }, }], "only": ["quickfix"] } }), ); assert_eq!( res, json!([{ "title": "Add a '.ts' extension.", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 0, "character": 19 }, "end": { "line": 0, "character": 24 } }, "severity": 3, "code": "no-local", "source": "deno", "message": format!( "Unable to load a local module: {}\nMaybe add a '.ts' extension.", temp_dir.join("a").url_file(), ), "data": { "specifier": temp_dir.join("a").url_file(), "to": temp_dir.join("a.ts").url_file(), "message": "Add a '.ts' extension.", }, }], "edit": { "changes": { temp_dir.join("file.ts").url_file(): [{ "range": { "start": { "line": 0, "character": 19 }, "end": { "line": 0, "character": 24 } }, "newText": "\"./a.ts\"" }] } } }]) ); client.shutdown(); } // Regression test for https://github.com/denoland/deno/issues/24457. #[test] fn lsp_byonm_js_import_resolves_to_dts() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .add_npm_env_vars() .build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.json", json!({ "unstable": ["byonm"], }) .to_string(), ); temp_dir.write( "package.json", json!({ "dependencies": { "postcss": "*", }, }) .to_string(), ); context.run_npm("install"); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.url().join("node_modules/postcss/lib/comment.d.ts").unwrap(), "languageId": "typescript", "version": 1, "text": temp_dir.path().join("node_modules/postcss/lib/comment.d.ts").read_to_string(), } })); assert_eq!(json!(diagnostics.all()), json!([])); client.shutdown(); } #[test] fn decorators_tc39() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write("deno.json", r#"{}"#); let mut client = context.new_lsp_command().build(); client.initialize_default(); let uri = temp_dir.url().join("main.ts").unwrap(); let diagnostics = client .did_open(json!({ "textDocument": { "uri": uri, "languageId": "typescript", "version": 1, "text": r#"// deno-lint-ignore no-explicit-any function logged(value: any, { kind, name }: { kind: string; name: string }) { if (kind === "method") { return function (...args: unknown[]) { console.log(`starting ${name} with arguments ${args.join(", ")}`); // @ts-ignore this has implicit any type const ret = value.call(this, ...args); console.log(`ending ${name}`); return ret; }; } } class C { @logged m(arg: number) { console.log("C.m", arg); } } new C().m(1); "# } })) .all(); assert_eq!(diagnostics.len(), 0); client.shutdown(); } #[test] fn decorators_ts() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.json", r#"{ "compilerOptions": { "experimentalDecorators": true } }"#, ); let mut client = context.new_lsp_command().build(); client.initialize_default(); let uri = temp_dir.url().join("main.ts").unwrap(); let diagnostics = client .did_open(json!({ "textDocument": { "uri": uri, "languageId": "typescript", "version": 1, "text": r#"// deno-lint-ignore-file function a() { console.log("@A evaluated"); return function ( _target: any, _propertyKey: string, descriptor: PropertyDescriptor, ) { console.log("@A called"); const fn = descriptor.value; descriptor.value = function () { console.log("fn() called from @A"); fn(); }; }; } class C { @a() static test() { console.log("C.test() called"); } } C.test(); "# } })) .all(); assert_eq!(json!(diagnostics), json!([])); client.shutdown(); } #[test] fn lsp_uses_lockfile_for_npm_initialization() { let context = TestContextBuilder::for_npm().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write("deno.json", "{}"); // use two npm packages here temp_dir.write("main.ts", "import 'npm:@denotest/esm-basic'; import 'npm:@denotest/cjs-default-export';"); context .new_command() .args("run main.ts") .run() .skip_output_check(); // remove one of the npm packages and let the other one be found via the lockfile temp_dir.write("main.ts", "import 'npm:@denotest/esm-basic';"); assert!(temp_dir.path().join("deno.lock").exists()); let mut client = context .new_lsp_command() .capture_stderr() .log_debug() .build(); client.initialize_default(); let mut skipping_count = 0; client.wait_until_stderr_line(|line| { if line.contains("Skipping npm resolution.") { skipping_count += 1; } assert!(!line.contains("Running npm resolution."), "Line: {}", line); line.contains("Server ready.") }); assert_eq!(skipping_count, 2); client.shutdown(); } #[test] fn lsp_cjs_internal_types_default_export() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .add_npm_env_vars() .build(); let temp_dir = context.temp_dir(); temp_dir.write("deno.json", r#"{}"#); temp_dir.write( "package.json", r#"{ "dependencies": { "@denotest/cjs-internal-types-default-export": "*" } }"#, ); context.run_npm("install"); let mut client = context.new_lsp_command().build(); client.initialize_default(); // this was previously being resolved as ESM and not correctly as CJS let node_modules_index_d_ts = temp_dir.path().join( "node_modules/@denotest/cjs-internal-types-default-export/index.d.ts", ); client.did_open(json!({ "textDocument": { "uri": node_modules_index_d_ts.url_file(), "languageId": "typescript", "version": 1, "text": node_modules_index_d_ts.read_to_string(), } })); let main_url = temp_dir.path().join("main.ts").url_file(); let diagnostics = client.did_open( json!({ "textDocument": { "uri": main_url, "languageId": "typescript", "version": 1, "text": "import * as mod from '@denotest/cjs-internal-types-default-export';\nmod.add(1, 2);", } }), ); // previously, diagnostic about `add` not being callable assert_eq!(json!(diagnostics.all()), json!([])); } #[test] fn lsp_cjs_import_dual() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .add_npm_env_vars() .build(); let temp_dir = context.temp_dir(); temp_dir.write("deno.json", r#"{}"#); temp_dir.write( "package.json", r#"{ "dependencies": { "@denotest/cjs-import-dual": "1" } }"#, ); context.run_npm("install"); let mut client = context.new_lsp_command().build(); client.initialize_default(); let main_url = temp_dir.path().join("main.ts").url_file(); let diagnostics = client.did_open( json!({ "textDocument": { "uri": main_url, "languageId": "typescript", "version": 1, // getKind() should resolve as "cjs" and cause a type checker error "text": "import { getKind } from 'npm:@denotest/cjs-import-dual@1';\nconst kind: 'esm' = getKind(); console.log(kind);", } }), ); assert_eq!( json!(diagnostics.all()), json!([{ "range": { "start": { "line": 1, "character": 6, }, "end": { "line": 1, "character": 10, }, }, "severity": 1, "code": 2322, "source": "deno-ts", "message": "Type '\"cjs\"' is not assignable to type '\"esm\"'.", }]) ); } #[test] fn lsp_type_commonjs() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .add_npm_env_vars() .build(); let temp_dir = context.temp_dir(); temp_dir.write("deno.json", r#"{}"#); temp_dir.write( "package.json", r#"{ "type": "commonjs", "dependencies": { "@denotest/dual-cjs-esm": "1" } }"#, ); context.run_npm("install"); let mut client = context.new_lsp_command().build(); client.initialize_default(); let main_url = temp_dir.path().join("main.ts").url_file(); let diagnostics = client.did_open( json!({ "textDocument": { "uri": main_url, "languageId": "typescript", "version": 1, // getKind() should resolve as "cjs" and cause a type checker error "text": "import mod = require('@denotest/dual-cjs-esm');\nconst kind: 'other' = mod.getKind(); console.log(kind);", } }), ); assert_eq!( json!(diagnostics.all()), json!([{ "range": { "start": { "line": 1, "character": 6, }, "end": { "line": 1, "character": 10, }, }, "severity": 1, "code": 2322, "source": "deno-ts", "message": "Type '\"cjs\"' is not assignable to type '\"other\"'.", }]) ); } #[test] fn lsp_ts_code_fix_any_param() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let src = "export function foo(param) { console.log(param); }"; let param_def = range_of("param", src); let main_url = temp_dir.path().join("main.ts").url_file(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": main_url, "languageId": "typescript", "version": 1, "text": src, } })); // make sure the "implicit any type" diagnostic is there for "param" assert_json_subset( json!(diagnostics.all()), json!([{ "range": param_def, "code": 7006, "message": "Parameter 'param' implicitly has an 'any' type." }]), ); // response is array of fixes let response = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": main_url, }, "range": lsp::Range { start: param_def.end, ..param_def }, "context": { "diagnostics": diagnostics.all(), } }), ); let fixes = response.as_array().unwrap(); // we're looking for the quick fix that pertains to our diagnostic, // specifically the "Infer parameter types from usage" fix for fix in fixes { let Some(diags) = fix.get("diagnostics") else { continue; }; let Some(fix_title) = fix.get("title").and_then(|s| s.as_str()) else { continue; }; if diags == &json!(diagnostics.all()) && fix_title == "Infer parameter types from usage" { // found it! return; } } panic!("failed to find 'Infer parameter types from usage' fix in fixes: {fixes:#?}"); } #[test] fn lsp_semantic_token_caching() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir().path(); let mut client: LspClient = context .new_lsp_command() .collect_perf() .set_root_dir(temp_dir.clone()) .build(); client.initialize_default(); let a = source_file( temp_dir.join("a.ts"), r#" export const a = 1; export const b = 2; export const bar = () => "bar"; function foo(fun: (number, number, number) => number, c: number) { const double = (x) => x * 2; return fun(double(a), b, c); }"#, ); client.did_open_file(&a); // requesting a range won't cache the tokens, so this will // be computed let res = client.write_request( "textDocument/semanticTokens/range", json!({ "textDocument": a.identifier(), "range": { "start": a.range_of("const bar").start, "end": a.range_of("}").end, } }), ); assert_eq!( client .perf_wait_for_measure("lsp.semantic_tokens_range") .measure_count("tsc.request.getEncodedSemanticClassifications"), 1, ); // requesting for the full doc should compute and cache the tokens let _full = client.write_request( "textDocument/semanticTokens/full", json!({ "textDocument": a.identifier(), }), ); assert_eq!( client .perf_wait_for_measure("lsp.semantic_tokens_full") .measure_count("tsc.request.getEncodedSemanticClassifications"), 2, ); // use the cached tokens let res_cached = client.write_request( "textDocument/semanticTokens/range", json!({ "textDocument": a.identifier(), "range": { "start": a.range_of("const bar").start, "end": a.range_of("}").end, } }), ); // make sure we actually used the cache assert_eq!( client .perf_wait_for_measure("lsp.semantic_tokens_range") .measure_count("tsc.request.getEncodedSemanticClassifications"), 2, ); assert_eq!(res, res_cached); } #[test] fn lsp_jsdoc_named_example() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir().path(); let mut client = context .new_lsp_command() .set_root_dir(temp_dir.clone()) .build(); client.initialize_default(); let main = source_file( temp_dir.join("main.ts"), r#" /** * @example Example1 * ```ts * foo(); * ``` */ export function foo(): number { return 1; } "#, ); let diagnostics = client.did_open_file(&main); assert_eq!(diagnostics.all().len(), 0); let hover = client.write_request( "textDocument/hover", json!({ "textDocument": main.identifier(), "position": main.range_of_nth(1, "foo").start, }), ); assert_json_subset( hover, json!({ "contents": [ { "language": "typescript", "value": "function foo(): number" }, "", // The example name `Example1` should not be enclosed in backticks "\n\n*@example* \nExample1\n```ts\nfoo();\n```" ] }), ); } #[test] fn lsp_wasm_module() { let context = TestContextBuilder::new() .use_temp_cwd() .use_http_server() .build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "import { add } from \"http://localhost:4545/wasm/math.wasm\";\nadd(1, '');\n" } })); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], "file:///a/file.ts"], }), ); let diagnostics = client.read_diagnostics(); assert_eq!( json!(diagnostics.all()), json!([ { "range": { "start": { "line": 1, "character": 7 }, "end": { "line": 1, "character": 9 } }, "severity": 1, "code": 2345, "source": "deno-ts", "message": "Argument of type 'string' is not assignable to parameter of type 'number'." } ]) ); client.shutdown(); }