diff options
author | Matt Mastracci <matthew@mastracci.com> | 2024-02-10 13:22:13 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-10 20:22:13 +0000 |
commit | f5e46c9bf2f50d66a953fa133161fc829cecff06 (patch) | |
tree | 8faf2f5831c1c7b11d842cd9908d141082c869a5 /tests/integration/lsp_tests.rs | |
parent | d2477f780630a812bfd65e3987b70c0d309385bb (diff) |
chore: move cli/tests/ -> tests/ (#22369)
This looks like a massive PR, but it's only a move from cli/tests ->
tests, and updates of relative paths for files.
This is the first step towards aggregate all of the integration test
files under tests/, which will lead to a set of integration tests that
can run without the CLI binary being built.
While we could leave these tests under `cli`, it would require us to
keep a more complex directory structure for the various test runners. In
addition, we have a lot of complexity to ignore various test files in
the `cli` project itself (cargo publish exclusion rules, autotests =
false, etc).
And finally, the `tests/` folder will eventually house the `test_ffi`,
`test_napi` and other testing code, reducing the size of the root repo
directory.
For easier review, the extremely large and noisy "move" is in the first
commit (with no changes -- just a move), while the remainder of the
changes to actual files is in the second commit.
Diffstat (limited to 'tests/integration/lsp_tests.rs')
-rw-r--r-- | tests/integration/lsp_tests.rs | 11240 |
1 files changed, 11240 insertions, 0 deletions
diff --git a/tests/integration/lsp_tests.rs b/tests/integration/lsp_tests.rs new file mode 100644 index 000000000..c9abae241 --- /dev/null +++ b/tests/integration/lsp_tests.rs @@ -0,0 +1,11240 @@ +// 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 test_util::assert_starts_with; +use test_util::deno_cmd_with_deno_dir; +use test_util::env_vars_for_npm_tests; +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"); + }); + + let diagnostics = client.did_open(json!({ + "textDocument": { + "uri": Url::from_file_path(temp_dir.path().join("test.ts")).unwrap(), + "languageId": "typescript", + "version": 1, + "text": "console.log(a);\n" + } + })); + + assert_eq!(diagnostics.all().len(), 0); + + client.shutdown(); +} + +#[test] +fn lsp_tsconfig_bad_config_path() { + let context = TestContextBuilder::new().use_temp_cwd().build(); + let mut client = context.new_lsp_command().build(); + client.initialize(|builder| { + builder + .set_config("bad_tsconfig.json") + .set_maybe_root_uri(None); + }); + let (method, maybe_params) = client.read_notification(); + assert_eq!(method, "window/showMessage"); + assert_eq!(maybe_params, Some(lsp::ShowMessageParams { + typ: lsp::MessageType::WARNING, + message: "The path to the configuration file (\"bad_tsconfig.json\") is not resolvable.".to_string() + })); + let diagnostics = client.did_open(json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "console.log(Deno.args);\n" + } + })); + assert_eq!(diagnostics.all().len(), 0); +} + +#[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.uri().join("test.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": "/// <reference types=\"./a.d.ts\" />\n\nconsole.log(a);\n" + } + })); + + assert_eq!(diagnostics.all().len(), 0); + + 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 = Url::from_file_path(temp_dir.path().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_remote() { + let context = TestContextBuilder::new() + .use_http_server() + .use_temp_cwd() + .build(); + let temp_dir = context.temp_dir(); + temp_dir.write( + "deno.json", + json!({ + "importMap": "http://localhost:4545/import_maps/import_map.json", + }) + .to_string(), + ); + 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.json"); + }); + client.write_request( + "workspace/executeCommand", + json!({ + "command": "deno.cache", + "arguments": [[], temp_dir.uri().join("file.ts").unwrap()], + }), + ); + + let diagnostics = client.did_open(json!({ + "textDocument": { + "uri": temp_dir.uri().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 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": "file:///a/file.ts", + "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.uri().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#"{ + "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.uri().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.uri().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.uri().join("deno.embedded_import_map.jsonc").unwrap(), + "type": 2 + }] + })); + + assert_eq!(client.read_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_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.uri().join("deno.jsonc").unwrap(), + "type": 2 + }] + })); + client.wait_until_stderr_line(|line| { + line.contains("Auto-resolved configuration file:") + }); + + let uri = temp_dir.uri().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.uri().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.uri().join("deno.jsonc").unwrap(), + "type": 2 + }] + })); + client.wait_until_stderr_line(|line| { + line.contains("Auto-resolved 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().uri_file(), + "type": 2 + }] + })); + + // this will discover the deno.json in the root + let search_line = format!( + "Auto-resolved configuration file: \"{}\"", + temp_dir.uri().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.uri().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().uri_file(), + "type": 2 + }] + })); + assert_eq!(client.read_diagnostics().all().len(), 0); + + 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.uri().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(); + 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(); + client.did_open(json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": r#"import "http://localhost:4545/run/002_hello.ts";"#, + }, + })); + client.write_request( + "workspace/executeCommand", + json!({ + "command": "deno.cache", + "arguments": [[], "file:///a/file.ts"], + }), + ); + assert!(temp_dir + .path() + .join("vendor/http_localhost_4545/run/002_hello.ts") + .exists()); + client.did_open(json!({ + "textDocument": { + "uri": temp_dir.uri().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.uri().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.uri()); + builder.set_enable_paths(vec!["./main_enabled.ts".to_string()]); + }); + + client.did_open(json!({ + "textDocument": { + "uri": temp_dir.uri().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.uri().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.uri().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.uri().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_change_deno_configuration_notification() { + 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(); + + temp_dir.write("deno.json", json!({}).to_string()); + client.did_change_watched_files(json!({ + "changes": [{ + "uri": temp_dir.uri().join("deno.json").unwrap(), + "type": 1, + }], + })); + let res = client + .read_notification_with_method::<Value>("deno/didChangeDenoConfiguration"); + assert_eq!( + res, + Some(json!({ + "changes": [{ + "scopeUri": temp_dir.uri(), + "fileUri": temp_dir.uri().join("deno.json").unwrap(), + "type": "added", + "configurationType": "denoJson" + }], + })) + ); + + temp_dir.write( + "deno.json", + json!({ "fmt": { "semiColons": false } }).to_string(), + ); + client.did_change_watched_files(json!({ + "changes": [{ + "uri": temp_dir.uri().join("deno.json").unwrap(), + "type": 2, + }], + })); + let res = client + .read_notification_with_method::<Value>("deno/didChangeDenoConfiguration"); + assert_eq!( + res, + Some(json!({ + "changes": [{ + "scopeUri": temp_dir.uri(), + "fileUri": temp_dir.uri().join("deno.json").unwrap(), + "type": "changed", + "configurationType": "denoJson" + }], + })) + ); + + temp_dir.remove_file("deno.json"); + client.did_change_watched_files(json!({ + "changes": [{ + "uri": temp_dir.uri().join("deno.json").unwrap(), + "type": 3, + }], + })); + let res = client + .read_notification_with_method::<Value>("deno/didChangeDenoConfiguration"); + assert_eq!( + res, + Some(json!({ + "changes": [{ + "scopeUri": temp_dir.uri(), + "fileUri": temp_dir.uri().join("deno.json").unwrap(), + "type": "removed", + "configurationType": "denoJson" + }], + })) + ); + + temp_dir.write("package.json", json!({}).to_string()); + client.did_change_watched_files(json!({ + "changes": [{ + "uri": temp_dir.uri().join("package.json").unwrap(), + "type": 1, + }], + })); + let res = client + .read_notification_with_method::<Value>("deno/didChangeDenoConfiguration"); + assert_eq!( + res, + Some(json!({ + "changes": [{ + "scopeUri": temp_dir.uri(), + "fileUri": temp_dir.uri().join("package.json").unwrap(), + "type": "added", + "configurationType": "packageJson" + }], + })) + ); + + temp_dir.write("package.json", json!({ "type": "module" }).to_string()); + client.did_change_watched_files(json!({ + "changes": [{ + "uri": temp_dir.uri().join("package.json").unwrap(), + "type": 2, + }], + })); + let res = client + .read_notification_with_method::<Value>("deno/didChangeDenoConfiguration"); + assert_eq!( + res, + Some(json!({ + "changes": [{ + "scopeUri": temp_dir.uri(), + "fileUri": temp_dir.uri().join("package.json").unwrap(), + "type": "changed", + "configurationType": "packageJson" + }], + })) + ); + + temp_dir.remove_file("package.json"); + client.did_change_watched_files(json!({ + "changes": [{ + "uri": temp_dir.uri().join("package.json").unwrap(), + "type": 3, + }], + })); + let res = client + .read_notification_with_method::<Value>("deno/didChangeDenoConfiguration"); + assert_eq!( + res, + Some(json!({ + "changes": [{ + "scopeUri": temp_dir.uri(), + "fileUri": temp_dir.uri().join("package.json").unwrap(), + "type": "removed", + "configurationType": "packageJson" + }], + })) + ); +} + +#[test] +fn lsp_deno_task() { + let context = TestContextBuilder::new().use_temp_cwd().build(); + let temp_dir = context.temp_dir(); + temp_dir.write( + "deno.jsonc", + r#"{ + "tasks": { + "build": "deno test", + "some:test": "deno bundle mod.ts" + } + }"#, + ); + + let mut client = context.new_lsp_command().build(); + client.initialize(|builder| { + builder.set_config("./deno.jsonc"); + }); + + let res = client.write_request("deno/task", json!(null)); + + assert_eq!( + res, + json!([ + { + "name": "build", + "detail": "deno test", + "sourceUri": temp_dir.uri().join("deno.jsonc").unwrap(), + }, { + "name": "some:test", + "detail": "deno bundle mod.ts", + "sourceUri": temp_dir.uri().join("deno.jsonc").unwrap(), + } + ]) + ); +} + +#[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)); +} + +#[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( + "import-map.json", + r#"{ + "imports": { + "/~/": "./lib/", + "fs": "https://example.com/fs/index.js", + "std/": "https://example.com/std@0.123.0/" + } +}"#, + ); + 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.uri().join("a.ts").unwrap(); + + client.did_open(json!({ + "textDocument": { + "uri": uri, + "languageId": "typescript", + "version": 1, + "text": "import * as a from \"/~/b.ts\";\nimport * as b from \"\"" + } + })); + + let res = client.get_completion( + &uri, + (1, 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": "std", + "kind": 19, + "detail": "(import map)", + "sortText": "std", + "insertText": "std", + "commitCharacters": ["\"", "'"], + }, { + "label": "fs", + "kind": 17, + "detail": "(import map)", + "sortText": "fs", + "insertText": "fs", + "commitCharacters": ["\"", "'"], + }, { + "label": "/~", + "kind": 19, + "detail": "(import map)", + "sortText": "/~", + "insertText": "/~", + "commitCharacters": ["\"", "'"], + } + ] + }) + ); + + client.write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": uri, + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { "line": 1, "character": 20 }, + "end": { "line": 1, "character": 20 } + }, + "text": "/~/" + } + ] + }), + ); + + let res = client.get_completion( + uri, + (1, 23), + json!({ + "triggerKind": 2, + "triggerCharacter": "/" + }), + ); + assert_eq!( + json!(res), + json!({ + "isIncomplete": false, + "items": [ + { + "label": "b.ts", + "kind": 9, + "detail": "(import map)", + "sortText": "1", + "filterText": "/~/b.ts", + "textEdit": { + "range": { + "start": { "line": 1, "character": 20 }, + "end": { "line": 1, "character": 23 } + }, + "newText": "/~/b.ts" + }, + "commitCharacters": ["\"", "'"], + } + ] + }) + ); + + 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 the\n[`std/flags`](https://deno.land/std/flags) module as part of the Deno\nstandard library.", + "\n\n*@category* - Runtime Environment", + ], + "range": { + "start": { "line": 0, "character": 17 }, + "end": { "line": 0, "character": 21 } + } + }) + ); + client.shutdown(); +} + +#[test] +fn lsp_hover_asset() { + 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(Date.now());\n" + } + })); + client.write_request( + "textDocument/definition", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "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." + ], + "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(|builder| { + builder.enable_inlay_hints(); + }); + 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!([ + { + "position": { "line": 0, "character": 21 }, + "label": ": string", + "kind": 1, + "paddingLeft": true + }, { + "position": { "line": 4, "character": 10 }, + "label": "b:", + "kind": 2, + "paddingRight": true + }, { + "position": { "line": 7, "character": 11 }, + "label": "= 0", + "paddingLeft": true + }, { + "position": { "line": 10, "character": 17 }, + "label": "string:", + "kind": 2, + "paddingRight": true + }, { + "position": { "line": 10, "character": 24 }, + "label": "radix:", + "kind": 2, + "paddingRight": true + }, { + "position": { "line": 12, "character": 15 }, + "label": ": number", + "kind": 1, + "paddingLeft": true + }, { + "position": { "line": 15, "character": 11 }, + "label": ": number", + "kind": 1, + "paddingLeft": true + }, { + "position": { "line": 18, "character": 18 }, + "label": "callbackfn:", + "kind": 2, + "paddingRight": true + }, { + "position": { "line": 18, "character": 20 }, + "label": ": string", + "kind": 1, + "paddingLeft": true + }, { + "position": { "line": 18, "character": 21 }, + "label": ": string", + "kind": 1, + "paddingLeft": true + } + ]) + ); +} + +#[test] +fn lsp_inlay_hints_not_enabled() { + let 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)); +} + +#[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.uri(); + + 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 { + root_specifier.clone() + } else { + ModuleSpecifier::parse( + root_specifier.as_str().strip_suffix('/').unwrap(), + ) + .unwrap() + }, + name: "project".to_string(), + }]) + .set_deno_enable(false); + }, + json!({ "deno": { + "enable": false, + "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.uri(); + + 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.ForeignLibraryInterface;\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.ForeignLibraryInterface" + }, + "**UNSTABLE**: New API, yet to be vetted.\n\nA foreign library interface descriptor.", + "\n\n*@category* - FFI", + ], + "range":{ + "start":{ "line":0, "character":14 }, + "end":{ "line":0, "character":37 } + } + }) + ); + 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.ForeignLibraryInterface;\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.ForeignLibraryInterface" + }, + "**UNSTABLE**: New API, yet to be vetted.\n\nA foreign library interface descriptor.", + "\n\n*@category* - FFI", + ], + "range":{ + "start":{ "line":0, "character":14 }, + "end":{ "line":0, "character":37 } + } + }) + ); + 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.uri().join("b.ts").unwrap(); + let c_specifier = temp_dir.uri().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\";\n\nconsole.log(a, b, c, d, e, f, g);\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 } + } + }) + ); +} + +// 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(); +} + +#[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_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/file.ts#L1,10) 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<string> {\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() { + 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": 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" + } + }]) + ); + 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_impl() { + 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(): 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": 4, "character": 6 }, + "end": { "line": 4, "character": 7 } + }, + "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.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::<Vec<lsp::CodeLens>>( + "textDocument/codeLens", + json!({ + "textDocument": { + "uri": "deno:/asset/lib.deno.shared_globals.d.ts" + } + }), + ); + assert!(res.len() > 50); + client.write_request_with_res_as::<lsp::CodeLens>( + "codeLens/resolve", + json!({ + "range": { + "start": { "line": 416, "character": 12 }, + "end": { "line": 416, "character": 19 } + }, + "data": { + "specifier": "asset:///lib.deno.shared_globals.d.ts", + "source": "references" + } + }), + ); + 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.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": 4, "character": 6 }, + "end": { "line": 4, "character": 7 } + }, + "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": 4, "character": 6 }, + "end": { "line": 4, "character": 7 } + }, + "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<void>" + }] + }] + } + }, { + "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<void>" + }, { + "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<void>" + }] + }] + }, + "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<Value> = 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-ts" -> "deno" -> "deno-lint". + assert_eq!( + res, + json!([ + { + "title": "Add async modifier to containing function", + "source": "deno-ts", + }, + { + "title": "Cache \"https://deno.land/x/a/mod.ts\" and its dependencies.", + "source": "deno", + }, + { + "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", + }, + ]) + ); +} + +#[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")); +} + +#[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_jsr_uncached() { + 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.uri().join("file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": r#"import "jsr:@foo/bar";"#, + }, + })); + // TODO(nayeemrmn): This should check if the jsr dep is cached and give a + // diagnostic. + assert_eq!(json!(diagnostics.all()), json!([])); + client.shutdown(); +} + +#[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": "no-cache-npm", + "source": "deno", + "message": "Uncached or missing npm package: chalk", + "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": "no-cache-npm", + "source": "deno", + "message": "Uncached or missing npm package: chalk", + "data": { "specifier": "npm:chalk" } + }], + "only": ["quickfix"] + } + }), + ); + assert_eq!( + res, + json!([{ + "title": "Cache \"npm:chalk\" and its dependencies.", + "kind": "quickfix", + "diagnostics": [{ + "range": { + "start": { "line": 0, "character": 18 }, + "end": { "line": 0, "character": 29 } + }, + "severity": 1, + "code": "no-cache-npm", + "source": "deno", + "message": "Uncached or missing npm package: chalk", + "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": "no-cache-npm", + "source": "deno", + "message": "Uncached or missing npm package: chalk", + "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": "no-cache-npm", + "source": "deno", + "message": "Uncached or missing npm package: chalk", + "data": { "specifier": "npm:chalk" }, + }, + ], + "command": { + "title": "", + "command": "deno.cache", + "arguments": [[], "file:///a/file.ts"], + } + }, + ]) + ); + 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.uri().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.uri().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.uri().join("file.ts").unwrap() }, + })); + assert_eq!(client.read_diagnostics().all(), vec![]); + + 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.uri().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.uri().join("file.ts").unwrap(), + ], + }), + ); + let res = client.write_request( + "textDocument/definition", + json!({ + "textDocument": { "uri": temp_dir.uri().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, + }, + }, + }]), + ); +} + +#[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.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 { DuckConfigOptions } from \"./file02.ts\";\n\n" + }] + }] + } + }, { + "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" + }] + }] + } + }, { + "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" + } + }]) + ); + 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 { DuckConfigOptions } from \"./file02.ts\";\n\n" + }] + }] + }, + "data": { + "specifier": "file:///a/file00.ts", + "fixId": "fixMissingImport" + } + }) + ); + + 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.uri().join("file00.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": temp_dir.read_to_string("file00.ts"), + } + })); + client.did_open(json!({ + "textDocument": { + "uri": temp_dir.uri().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.uri().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.uri().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.uri().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.uri().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.uri().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.uri().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.uri().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.uri().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.uri().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", + }], + }], + }, + }]), + ); +} + +#[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 | undefined;\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 Environment" + }, + "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": "file:///a/b.ts", + "languageId": "typescript", + "version": 1, + "text": "export const foo = \"foo\";\n", + } + })); + client.did_open(json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "export {};\n\n", + } + })); + let list = client.get_completion_list( + "file:///a/file.ts", + (2, 0), + json!({ "triggerKind": 1 }), + ); + assert!(!list.is_incomplete); + let item = list.items.iter().find(|item| item.label == "foo"); + let Some(item) = item else { + panic!("completions items missing 'foo' symbol"); + }; + let mut item_value = serde_json::to_value(item).unwrap(); + item_value["data"]["tsc"]["data"]["exportMapKey"] = + serde_json::Value::String("".to_string()); + + let req = json!({ + "label": "foo", + "labelDetails": { + "description": "./b.ts", + }, + "kind": 6, + "sortText": "16_0", + "commitCharacters": [ + ".", + ",", + ";", + "(" + ], + "data": { + "tsc": { + "specifier": "file:///a/file.ts", + "position": 12, + "name": "foo", + "source": "./b.ts", + "data": { + "exportName": "foo", + "exportMapKey": "", + "moduleSpecifier": "./b.ts", + "fileName": "file:///a/b.ts" + }, + "useCodeSnippet": false + } + } + }); + assert_eq!(item_value, req); + + let res = client.write_request("completionItem/resolve", req); + assert_eq!( + res, + json!({ + "label": "foo", + "labelDetails": { + "description": "./b.ts", + }, + "kind": 6, + "detail": "const foo: \"foo\"", + "documentation": { + "kind": "markdown", + "value": "" + }, + "sortText": "16_0", + "additionalTextEdits": [ + { + "range": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 0, "character": 0 } + }, + "newText": "import { foo } from \"./b.ts\";\n\n" + } + ] + }) + ); +} + +#[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" + }] + }] + } + }]) + ); +} + +#[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], + }) + ); +} + +#[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": "file:///a/file.ts", + "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", + ], + "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": "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": "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 \"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": "file:///a/b.ts", + "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": "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": "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": "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 \"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": "file:///a/d.ts", + "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": "file:///a/e.ts", + "languageId": "typescript", + "version": 1, + "text": "printH", + } + })); + + let list = client.get_completion_list( + "file:///a/e.ts", + (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": "file:///a/f.ts", + "languageId": "typescript", + "version": 1, + "text": "printHello", + } + })); + let diagnostics = diagnostics + .messages_with_file_and_source("file:///a/f.ts", "deno-ts") + .diagnostics; + let res = client.write_request( + "textDocument/codeAction", + json!(json!({ + "textDocument": { + "uri": "file:///a/f.ts" + }, + "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": "file:///a/f.ts", + "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": "file:///a/nested_path.ts", + "languageId": "typescript", + "version": 1, + "text": "entry", + } + })); + let list = client.get_completion_list( + "file:///a/nested_path.ts", + (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" + } + ] + }) + ); +} + +#[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 <A t\n}", + } + }), + ); + let list = client.get_completion_list( + "file:///a/a.tsx", + (5, 13), + json!({ "triggerKind": 1 }), + ); + assert!(!list.is_incomplete); + assert_eq!( + json!(list), + json!({ + "isIncomplete": false, + "items": [ + { + "label": "type", + "kind": 5, + "sortText": "11", + "filterText": "type=\"$1\"", + "insertText": "type=\"$1\"", + "insertTextFormat": 2, + "commitCharacters": [ + ".", + ",", + ";", + "(" + ], + "data": { + "tsc": { + "specifier": "file:///a/a.tsx", + "position": 87, + "name": "type", + "useCodeSnippet": false + } + } + } + ] + }) + ); + + let res = client.write_request( + "completionItem/resolve", + json!({ + "label": "type", + "kind": 5, + "sortText": "11", + "filterText": "type=\"$1\"", + "insertText": "type=\"$1\"", + "insertTextFormat": 2, + "commitCharacters": [ + ".", + ",", + ";", + "(" + ], + "data": { + "tsc": { + "specifier": "file:///a/a.tsx", + "position": 87, + "name": "type", + "useCodeSnippet": false + } + } + }), + ); + assert_eq!( + res, + json!({ + "label": "type", + "kind": 5, + "detail": "(property) type: string", + "documentation": { + "kind": "markdown", + "value": "" + }, + "sortText": "11", + "filterText": "type=\"$1\"", + "insertText": "type=\"$1\"", + "insertTextFormat": 2 + }) + ); +} + +#[test] +fn lsp_completions_no_snippet() { + let 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(); + doc.completion = None; + }); + }); + 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 <A t\n}", + } + }), + ); + let list = client.get_completion_list( + "file:///a/a.tsx", + (5, 13), + json!({ "triggerKind": 1 }), + ); + assert!(!list.is_incomplete); + assert_eq!( + json!(list), + json!({ + "isIncomplete": false, + "items": [ + { + "label": "type", + "kind": 5, + "sortText": "11", + "commitCharacters": [ + ".", + ",", + ";", + "(" + ], + "data": { + "tsc": { + "specifier": "file:///a/a.tsx", + "position": 87, + "name": "type", + "useCodeSnippet": false + } + } + } + ] + }) + ); +} + +#[test] +fn lsp_completions_npm() { + 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 cjsDefault from 'npm:@denotest/cjs-default-export';import chalk from 'npm:chalk';\n\n", + } + }), + ); + client.write_request( + "workspace/executeCommand", + json!({ + "command": "deno.cache", + "arguments": [ + ["npm:@denotest/cjs-default-export", "npm:chalk"], + "file:///a/file.ts", + ], + }), + ); + + // check importing a cjs default import + client.write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { "line": 2, "character": 0 }, + "end": { "line": 2, "character": 0 } + }, + "text": "cjsDefault." + } + ] + }), + ); + client.read_diagnostics(); + + let list = client.get_completion_list( + "file:///a/file.ts", + (2, 11), + json!({ + "triggerKind": 2, + "triggerCharacter": "." + }), + ); + assert!(!list.is_incomplete); + assert_eq!(list.items.len(), 3); + assert!(list.items.iter().any(|i| i.label == "default")); + assert!(list.items.iter().any(|i| i.label == "MyClass")); + + 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 mut client = context.new_lsp_command().build(); + client.initialize_default(); + + // create other.ts, which re-exports an npm specifier + client.deno_dir().write( + "other.ts", + "export { default as chalk } from 'npm:chalk@5';", + ); + + // cache the other.ts file to the DENO_DIR + let deno = deno_cmd_with_deno_dir(client.deno_dir()) + .current_dir(client.deno_dir().path()) + .arg("cache") + .arg("--quiet") + .arg("other.ts") + .envs(env_vars_for_npm_tests()) + .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!(stdout.is_empty()); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!(stderr.is_empty()); + + // open main.ts, which imports other.ts (unopened) + let main_url = + ModuleSpecifier::from_file_path(client.deno_dir().path().join("main.ts")) + .unwrap(); + client.did_open(json!({ + "textDocument": { + "uri": main_url, + "languageId": "typescript", + "version": 1, + "text": "import { chalk } from './other.ts';\n\n", + } + })); + + client.write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": main_url, + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { "line": 2, "character": 0 }, + "end": { "line": 2, "character": 0 } + }, + "text": "chalk." + } + ] + }), + ); + client.read_diagnostics(); + + // now ensure completions work + let list = client.get_completion_list( + main_url, + (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")); +} + +#[test] +fn lsp_completions_node_specifier() { + 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::<Vec<_>>(); + 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::<Vec<_>>(); + + // 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 ../\nIf 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("no-cache-npm".to_string())) + }) + .collect::<Vec<_>>(); + + 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": "no-cache-npm", + "source": "deno", + "message": "Uncached or missing npm package: @types/node" + } + ]) + ); + + 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_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 (method, res) = client.read_notification(); + assert_eq!(method, "deno/registryState"); + assert_eq!( + res, + Some(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_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: Url::parse("file:///a/file.ts").unwrap(), + diagnostics: vec![ + lsp::Diagnostic { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 19 + }, + end: lsp::Position { + line: 0, + character: 60 + } + }, + severity: Some(lsp::DiagnosticSeverity::WARNING), + code: Some(lsp::NumberOrString::String("deno-warn".to_string())), + source: Some("deno".to_string()), + message: "foobar".to_string(), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 19 + }, + end: lsp::Position { + line: 0, + character: 60 + } + }, + severity: Some(lsp::DiagnosticSeverity::INFORMATION), + code: Some(lsp::NumberOrString::String("redirect".to_string())), + source: Some("deno".to_string()), + message: "The import of \"http://127.0.0.1:4545/x_deno_warning.js\" was redirected to \"http://127.0.0.1:4545/lsp/x_deno_warning_redirect.js\".".to_string(), + data: Some(json!({"specifier": "http://127.0.0.1:4545/x_deno_warning.js", "redirect": "http://127.0.0.1:4545/lsp/x_deno_warning_redirect.js"})), + ..Default::default() + } + ], + version: Some(1), + } + ); + 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_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": "/// <reference types=\"https://example.com/a/b.d.ts\" />\n/// <reference path=\"https://example.com/a/c.ts\"\n\n// @deno-types=https://example.com/a/d.d.ts\nimport * as d from \"https://example.com/a/d.js\";\n\n// @deno-types=\"https://example.com/a/e.d.ts\"\nimport * as e from \"https://example.com/a/e.js\";\n\nconsole.log(d, e);\n" + } + }), + ); + + client.write_request( + "textDocument/documentSymbol", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + } + }), + ); + assert_eq!(diagnostics.all().len(), 5); + client.shutdown(); +} + +#[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!(diagnostics.all().len(), 0); // no diagnostics now + + client.shutdown(); + assert_eq!(client.queue_len(), 0); +} + +// 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.uri().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.uri().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.uri().join("file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": "", + }, + })); + temp_dir.write("file.ts", r#""#); + client.did_save(json!({ + "textDocument": { + "uri": temp_dir.uri().join("file.ts").unwrap(), + }, + })); + let diagnostics = client.read_diagnostics(); + assert_eq!(diagnostics.all(), vec![]); + 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, + pub count: u32, + pub average_duration: u32, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct PerformanceAverages { + averages: Vec<PerformanceAverage>, +} + +#[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::<PerformanceAverages>( + "deno/performance", + json!(null), + ); + let mut averages = res + .averages + .iter() + .map(|a| a.name.as_str()) + .collect::<Vec<_>>(); + 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_import_map", + "lsp.update_registries", + "lsp.update_tsconfig", + "tsc.host.$configure", + "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_project_version", + "tsc.op.op_script_names", + "tsc.op.op_script_version", + "tsc.request.$configure", + "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": { + "files": { + "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.uri().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": { + "files": { + "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.uri().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_path = context.temp_dir().path(); + // Also test out using a non-json file extension here. + // What should matter is the language identifier. + let lock_file_path = temp_dir_path.join("file.lock"); + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + client.did_open(json!({ + "textDocument": { + "uri": lock_file_path.uri_file(), + "languageId": "json", + "version": 1, + "text": "{\"key\":\"value\"}" + } + })); + + let res = client.write_request( + "textDocument/formatting", + json!({ + "textDocument": { + "uri": lock_file_path.uri_file(), + }, + "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_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(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(json!({ + "textDocument": { + "uri": temp_dir.uri().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.uri().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 markdown_file = context.temp_dir().path().join("file.md"); + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + client.did_open(json!({ + "textDocument": { + "uri": markdown_file.uri_file(), + "languageId": "markdown", + "version": 1, + "text": "# Hello World" + } + })); + + let res = client.write_request( + "textDocument/formatting", + json!({ + "textDocument": { + "uri": markdown_file.uri_file() + }, + "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_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.uri_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.uri_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(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": false, + } })); + + 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<never>.map<U>(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.*@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": "fields", + "kind": 6, + "location": { + "uri": "deno:/asset/lib.deno.unstable.d.ts", + "range": null, + }, + "containerName": "CalendarProtocol" + }, + { + "name": "fields", + "kind": 6, + "location": { + "uri": "deno:/asset/lib.deno.unstable.d.ts", + "range": null, + }, + "containerName": "Calendar" + }, + { + "name": "ClassFieldDecoratorContext", + "kind": 11, + "location": { + "uri": "deno:/asset/lib.decorators.d.ts", + "range": null, + }, + "containerName": "" + }, + { + "name": "dateFromFields", + "kind": 6, + "location": { + "uri": "deno:/asset/lib.deno.unstable.d.ts", + "range": null, + }, + "containerName": "CalendarProtocol" + }, + { + "name": "dateFromFields", + "kind": 6, + "location": { + "uri": "deno:/asset/lib.deno.unstable.d.ts", + "range": null, + }, + "containerName": "Calendar" + }, + { + "name": "getISOFields", + "kind": 6, + "location": { + "uri": "deno:/asset/lib.deno.unstable.d.ts", + "range": null, + }, + "containerName": "PlainDate" + }, + { + "name": "getISOFields", + "kind": 6, + "location": { + "uri": "deno:/asset/lib.deno.unstable.d.ts", + "range": null, + }, + "containerName": "PlainDateTime" + }, + { + "name": "getISOFields", + "kind": 6, + "location": { + "uri": "deno:/asset/lib.deno.unstable.d.ts", + "range": null, + }, + "containerName": "PlainMonthDay" + }, + { + "name": "getISOFields", + "kind": 6, + "location": { + "uri": "deno:/asset/lib.deno.unstable.d.ts", + "range": null, + }, + "containerName": "PlainTime" + }, + { + "name": "getISOFields", + "kind": 6, + "location": { + "uri": "deno:/asset/lib.deno.unstable.d.ts", + "range": null, + }, + "containerName": "PlainYearMonth" + }, + { + "name": "getISOFields", + "kind": 6, + "location": { + "uri": "deno:/asset/lib.deno.unstable.d.ts", + "range": null, + }, + "containerName": "ZonedDateTime" + }, + { + "name": "mergeFields", + "kind": 6, + "location": { + "uri": "deno:/asset/lib.deno.unstable.d.ts", + "range": null, + }, + "containerName": "CalendarProtocol" + }, + { + "name": "mergeFields", + "kind": 6, + "location": { + "uri": "deno:/asset/lib.deno.unstable.d.ts", + "range": null, + }, + "containerName": "Calendar" + }, + { + "name": "monthDayFromFields", + "kind": 6, + "location": { + "uri": "deno:/asset/lib.deno.unstable.d.ts", + "range": null, + }, + "containerName": "CalendarProtocol" + }, + { + "name": "monthDayFromFields", + "kind": 6, + "location": { + "uri": "deno:/asset/lib.deno.unstable.d.ts", + "range": null, + }, + "containerName": "Calendar" + }, + { + "name": "PlainDateISOFields", + "kind": 5, + "location": { + "uri": "deno:/asset/lib.deno.unstable.d.ts", + "range": null, + }, + "containerName": "Temporal" + }, + { + "name": "PlainDateTimeISOFields", + "kind": 5, + "location": { + "uri": "deno:/asset/lib.deno.unstable.d.ts", + "range": null, + }, + "containerName": "Temporal" + }, + { + "name": "PlainTimeISOFields", + "kind": 5, + "location": { + "uri": "deno:/asset/lib.deno.unstable.d.ts", + "range": null, + }, + "containerName": "Temporal" + }, + { + "name": "yearMonthFromFields", + "kind": 6, + "location": { + "uri": "deno:/asset/lib.deno.unstable.d.ts", + "range": null, + }, + "containerName": "CalendarProtocol" + }, + { + "name": "yearMonthFromFields", + "kind": 6, + "location": { + "uri": "deno:/asset/lib.deno.unstable.d.ts", + "range": null, + }, + "containerName": "Calendar" + }, + { + "name": "ZonedDateTimeISOFields", + "kind": 5, + "location": { + "uri": "deno:/asset/lib.deno.unstable.d.ts", + "range": null, + }, + "containerName": "Temporal" + } + ]) + ); + 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_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": "file:///a/file.ts", + "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": { + "files": { + "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 <A></A>; +} +", + } + })); + 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 } + } + }) + ); + 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.uri().join("file.tsx").unwrap(), + "languageId": "typescriptreact", + "version": 1, + "text": " + export function Foo() { + return <div></div>; + } + ", + }, + })); + // 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. + 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. + diagnostics = client.did_open(json!({ + "textDocument": { + "uri": temp_dir.uri().join("file.tsx").unwrap(), + "languageId": "typescriptreact", + "version": 1, + "text": " + export function Foo() { + return <div></div>; + } + ", + }, + })); + } + assert_eq!(diagnostics.all(), vec![]); + client.shutdown(); +} + +#[derive(Debug, Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct TestData { + id: String, + label: String, + steps: Option<Vec<TestData>>, + range: Option<lsp::Range>, +} + +#[derive(Debug, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +enum TestModuleNotificationKind { + Insert, + Replace, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct TestModuleNotificationParams { + text_document: lsp::TextDocumentIdentifier, + kind: TestModuleNotificationKind, + label: String, + tests: Vec<TestData>, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct EnqueuedTestModule { + text_document: lsp::TextDocumentIdentifier, + ids: Vec<String>, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct TestRunResponseParams { + enqueued: Vec<EnqueuedTestModule>, +} + +#[test] +fn lsp_testing_api() { + let 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", () => {}); + } +}); +"#; + temp_dir.write("./test.ts", contents); + temp_dir.write("./deno.jsonc", "{}"); + let specifier = temp_dir.uri().join("test.ts").unwrap(); + + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + + client.did_open(json!({ + "textDocument": { + "uri": specifier, + "languageId": "typescript", + "version": 1, + "text": contents, + } + })); + + let notification = + client.read_notification_with_method::<Value>("deno/testModule"); + let params: TestModuleNotificationParams = + serde_json::from_value(notification.unwrap()).unwrap(); + assert_eq!(params.text_document.uri, specifier); + assert_eq!(params.kind, TestModuleNotificationKind::Replace); + assert_eq!(params.label, "test.ts"); + assert_eq!(params.tests.len(), 1); + let test = ¶ms.tests[0]; + assert_eq!(test.label, "test a"); + assert_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::<TestRunResponseParams>( + "deno/testRun", + json!({ + "id": 1, + "kind": "run", + }), + ); + assert_eq!(res.enqueued.len(), 1); + assert_eq!(res.enqueued[0].text_document.uri, specifier); + assert_eq!(res.enqueued[0].ids.len(), 1); + let id = res.enqueued[0].ids[0].clone(); + let notification = + client.read_notification_with_method::<Value>("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::<Value>("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::<Value>("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::<Value>("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::<Value>("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::<Value>("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.uri().join("test.ts").unwrap(), + "version": 2 + }, + "contentChanges": [{ "text": "" }], + }), + ); + + assert_eq!(client.read_diagnostics().all().len(), 0); + + let notification = + client.read_notification_with_method::<Value>("deno/testModuleDelete"); + assert_eq!( + notification, + Some(json!({ + "textDocument": { + "uri": temp_dir.uri().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.uri(); + 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.uri(); + 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.uri(); + 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 = Url::from_file_path(temp_dir.path().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(); + + // 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.uri().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()); + + temp_dir.write( + temp_dir.path().join("deno.json"), + "{ \"nodeModulesDir\": true, \"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": false, + } })); + }; + 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!(!temp_dir.path().join("deno.lock").exists()); + + // now add a lockfile and cache + temp_dir.write( + temp_dir.path().join("deno.json"), + "{ \"nodeModulesDir\": true }\n", + ); + refresh_config(&mut client); + cache(&mut client); + + let diagnostics = client.read_diagnostics(); + assert_eq!(diagnostics.all().len(), 0, "{:#?}", diagnostics); + + assert!(temp_dir.path().join("deno.lock").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.uri().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()); + + temp_dir.write( + temp_dir.path().join("deno.json"), + "{ \"vendor\": true, \"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": false, + } })); + }; + refresh_config(&mut client); + + let diagnostics = client.read_diagnostics(); + assert_eq!(diagnostics.all().len(), 0, "{:#?}", diagnostics); // cached + + // no caching necessary because it was already cached. It should exist now + 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.uri_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_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.uri().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::<Vec<_>>(); + + // 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").uri_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_sloppy_imports_warn() { + 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"] }"#); + // should work when exists on the fs and when doesn't + 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.uri_dir()); + }); + client.did_open(json!({ + "textDocument": { + "uri": temp_dir.join("b.ts").uri_file(), + "languageId": "typescript", + "version": 1, + "text": "export class B {}", + }, + })); + let diagnostics = client.did_open(json!({ + "textDocument": { + "uri": temp_dir.join("file.ts").uri_file(), + "languageId": "typescript", + "version": 1, + "text": "import * as a from './a';\nimport * as b from './b.js';\nconsole.log(a)\nconsole.log(b);\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::INFORMATION), + code: Some(lsp::NumberOrString::String("redirect".to_string())), + source: Some("deno".to_string()), + message: format!( + "The import of \"{}\" was redirected to \"{}\".", + temp_dir.join("a").uri_file(), + temp_dir.join("a.ts").uri_file() + ), + data: Some(json!({ + "specifier": temp_dir.join("a").uri_file(), + "redirect": temp_dir.join("a.ts").uri_file() + })), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range { + start: lsp::Position { + line: 1, + character: 19 + }, + end: lsp::Position { + line: 1, + character: 27 + } + }, + severity: Some(lsp::DiagnosticSeverity::INFORMATION), + code: Some(lsp::NumberOrString::String("redirect".to_string())), + source: Some("deno".to_string()), + message: format!( + "The import of \"{}\" was redirected to \"{}\".", + temp_dir.join("b.js").uri_file(), + temp_dir.join("b.ts").uri_file() + ), + data: Some(json!({ + "specifier": temp_dir.join("b.js").uri_file(), + "redirect": temp_dir.join("b.ts").uri_file() + })), + ..Default::default() + } + ], + version: Some(1), + } + ); + + let res = client.write_request( + "textDocument/codeAction", + json!({ + "textDocument": { + "uri": temp_dir.join("file.ts").uri_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": "redirect", + "source": "deno", + "message": format!( + "The import of \"{}\" was redirected to \"{}\".", + temp_dir.join("a").uri_file(), + temp_dir.join("a.ts").uri_file() + ), + "data": { + "specifier": temp_dir.join("a").uri_file(), + "redirect": temp_dir.join("a.ts").uri_file(), + }, + }], + "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": 24 } + }, + "severity": 3, + "code": "redirect", + "source": "deno", + "message": format!( + "The import of \"{}\" was redirected to \"{}\".", + temp_dir.join("a").uri_file(), + temp_dir.join("a.ts").uri_file() + ), + "data": { + "specifier": temp_dir.join("a").uri_file(), + "redirect": temp_dir.join("a.ts").uri_file() + }, + }], + "edit": { + "changes": { + temp_dir.join("file.ts").uri_file(): [{ + "range": { + "start": { "line": 0, "character": 19 }, + "end": { "line": 0, "character": 24 } + }, + "newText": "\"./a.ts\"" + }] + } + } + }]) + ); + + client.shutdown(); +} + +#[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.uri_dir()); + }); + let diagnostics = client.did_open(json!({ + "textDocument": { + "uri": temp_dir.join("file.ts").uri_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").uri_file(), + ), + data: Some(json!({ + "specifier": temp_dir.join("a").uri_file(), + "to": temp_dir.join("a.ts").uri_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").uri_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").uri_file(), + ), + "data": { + "specifier": temp_dir.join("a").uri_file(), + "to": temp_dir.join("a.ts").uri_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").uri_file(), + ), + "data": { + "specifier": temp_dir.join("a").uri_file(), + "to": temp_dir.join("a.ts").uri_file(), + "message": "Add a '.ts' extension.", + }, + }], + "edit": { + "changes": { + temp_dir.join("file.ts").uri_file(): [{ + "range": { + "start": { "line": 0, "character": 19 }, + "end": { "line": 0, "character": 24 } + }, + "newText": "\"./a.ts\"" + }] + } + } + }]) + ); + 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 = Url::from_file_path(temp_dir.path().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 = Url::from_file_path(temp_dir.path().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!(diagnostics.len(), 0); + + client.shutdown(); +} |