summaryrefslogtreecommitdiff
path: root/tests/integration/lsp_tests.rs
diff options
context:
space:
mode:
authorMatt Mastracci <matthew@mastracci.com>2024-02-10 13:22:13 -0700
committerGitHub <noreply@github.com>2024-02-10 20:22:13 +0000
commitf5e46c9bf2f50d66a953fa133161fc829cecff06 (patch)
tree8faf2f5831c1c7b11d842cd9908d141082c869a5 /tests/integration/lsp_tests.rs
parentd2477f780630a812bfd65e3987b70c0d309385bb (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.rs11240
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&#8203;://127.0.0.1:4545/xTypeScriptTypes.js\n\n**Types**: http&#8203;://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&#8203;://127.0.0.1:4545/subdir/type_reference.js\n\n**Types**: http&#8203;://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&#8203;://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&#8203;:///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&#8203;://127.0.0.1:4545/xTypeScriptTypes.js\n\n**Types**: http&#8203;://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&#8203;://127.0.0.1:4545/xTypeScriptTypes.js\n\n**Types**: http&#8203;://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&#8203;://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&#8203;://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&#8203;://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&#8203;://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 = &params.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();
+}