// 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": "/// \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::("deno/didChangeDenoConfiguration"); assert_eq!( res, Some(json!({ "changes": [{ "uri": temp_dir.uri().join("deno.json").unwrap(), "type": 1, "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::("deno/didChangeDenoConfiguration"); assert_eq!( res, Some(json!({ "changes": [{ "uri": temp_dir.uri().join("deno.json").unwrap(), "type": 2, "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::("deno/didChangeDenoConfiguration"); assert_eq!( res, Some(json!({ "changes": [{ "uri": temp_dir.uri().join("deno.json").unwrap(), "type": 3, "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::("deno/didChangeDenoConfiguration"); assert_eq!( res, Some(json!({ "changes": [{ "uri": temp_dir.uri().join("package.json").unwrap(), "type": 1, "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::("deno/didChangeDenoConfiguration"); assert_eq!( res, Some(json!({ "changes": [{ "uri": temp_dir.uri().join("package.json").unwrap(), "type": 2, "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::("deno/didChangeDenoConfiguration"); assert_eq!( res, Some(json!({ "changes": [{ "uri": temp_dir.uri().join("package.json").unwrap(), "type": 3, "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_disabled() { 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": "type Deno.ForeignLibraryInterface = /*unresolved*/ any", }, "", ], "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| { builder.set_unstable(true); }); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "type _ = Deno.ForeignLibraryInterface;\n" } })); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 0, "character": 14 } }), ); assert_eq!( res, json!({ "contents":[ { "language":"typescript", "value":"interface Deno.ForeignLibraryInterface" }, "**UNSTABLE**: New API, yet to be vetted.\n\nA foreign library interface descriptor.", "\n\n*@category* - FFI", ], "range":{ "start":{ "line":0, "character":14 }, "end":{ "line":0, "character":37 } } }) ); client.shutdown(); } #[test] fn lsp_hover_change_mbc() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "const a = `编写软件很难`;\nconst b = `👍🦕😃`;\nconsole.log(a, b);\n" } }), ); client.write_notification( "textDocument/didChange", json!({ "textDocument": { "uri": "file:///a/file.ts", "version": 2 }, "contentChanges": [ { "range": { "start": { "line": 1, "character": 11 }, "end": { "line": 1, // the LSP uses utf16 encoded characters indexes, so // after the deno emoji is character index 15 "character": 15 } }, "text": "" } ] }), ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 2, "character": 15 } }), ); assert_eq!( res, json!({ "contents": [ { "language": "typescript", "value": "const b: \"😃\"", }, "", ], "range": { "start": { "line": 2, "character": 15, }, "end": { "line": 2, "character": 16, }, } }) ); client.shutdown(); } #[test] fn lsp_hover_closed_document() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); temp_dir.write("a.ts", r#"export const a = "a";"#); temp_dir.write("b.ts", r#"export * from "./a.ts";"#); temp_dir.write("c.ts", "import { a } from \"./b.ts\";\nconsole.log(a);\n"); let b_specifier = temp_dir.uri().join("b.ts").unwrap(); let c_specifier = temp_dir.uri().join("c.ts").unwrap(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": b_specifier, "languageId": "typescript", "version": 1, "text": r#"export * from "./a.ts";"# } })); client.did_open(json!({ "textDocument": { "uri": c_specifier, "languageId": "typescript", "version": 1, "text": "import { a } from \"./b.ts\";\nconsole.log(a);\n", } })); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": c_specifier, }, "position": { "line": 0, "character": 10 } }), ); assert_eq!( res, json!({ "contents": [ { "language": "typescript", "value": "(alias) const a: \"a\"\nimport a" }, "" ], "range": { "start": { "line": 0, "character": 9 }, "end": { "line": 0, "character": 10 } } }) ); client.write_notification( "textDocument/didClose", json!({ "textDocument": { "uri": b_specifier, } }), ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": c_specifier, }, "position": { "line": 0, "character": 10 } }), ); assert_eq!( res, json!({ "contents": [ { "language": "typescript", "value": "(alias) const a: \"a\"\nimport a" }, "" ], "range": { "start": { "line": 0, "character": 9 }, "end": { "line": 0, "character": 10 } } }) ); client.shutdown(); } #[test] fn lsp_hover_dependency() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file_01.ts", "languageId": "typescript", "version": 1, "text": "export const a = \"a\";\n", } })); client.did_open( json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "import * as a from \"http://127.0.0.1:4545/xTypeScriptTypes.js\";\n// @deno-types=\"http://127.0.0.1:4545/type_definitions/foo.d.ts\"\nimport * as b from \"http://127.0.0.1:4545/type_definitions/foo.js\";\nimport * as c from \"http://127.0.0.1:4545/subdir/type_reference.js\";\nimport * as d from \"http://127.0.0.1:4545/subdir/mod1.ts\";\nimport * as e from \"data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=\";\nimport * as f from \"./file_01.ts\";\nimport * as g from \"http://localhost:4545/x/a/mod.ts\";\n\nconsole.log(a, b, c, d, e, f, g);\n" } }), ); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], "file:///a/file.ts"], }), ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "position": { "line": 0, "character": 28 } }), ); assert_eq!( res, json!({ "contents": { "kind": "markdown", "value": "**Resolved Dependency**\n\n**Code**: http​://127.0.0.1:4545/xTypeScriptTypes.js\n\n**Types**: http​://127.0.0.1:4545/xTypeScriptTypes.d.ts\n" }, "range": { "start": { "line": 0, "character": 19 }, "end":{ "line": 0, "character": 62 } } }) ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "position": { "line": 3, "character": 28 } }), ); assert_eq!( res, json!({ "contents": { "kind": "markdown", "value": "**Resolved Dependency**\n\n**Code**: http​://127.0.0.1:4545/subdir/type_reference.js\n\n**Types**: http​://127.0.0.1:4545/subdir/type_reference.d.ts\n" }, "range": { "start": { "line": 3, "character": 19 }, "end":{ "line": 3, "character": 67 } } }) ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "position": { "line": 4, "character": 28 } }), ); assert_eq!( res, json!({ "contents": { "kind": "markdown", "value": "**Resolved Dependency**\n\n**Code**: http​://127.0.0.1:4545/subdir/mod1.ts\n" }, "range": { "start": { "line": 4, "character": 19 }, "end":{ "line": 4, "character": 57 } } }) ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "position": { "line": 5, "character": 28 } }), ); assert_eq!( res, json!({ "contents": { "kind": "markdown", "value": "**Resolved Dependency**\n\n**Code**: _(a data url)_\n" }, "range": { "start": { "line": 5, "character": 19 }, "end":{ "line": 5, "character": 132 } } }) ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "position": { "line": 6, "character": 28 } }), ); assert_eq!( res, json!({ "contents": { "kind": "markdown", "value": "**Resolved Dependency**\n\n**Code**: file​:///a/file_01.ts\n" }, "range": { "start": { "line": 6, "character": 19 }, "end":{ "line": 6, "character": 33 } } }) ); } // This tests for a regression covered by denoland/deno#12753 where the lsp was // unable to resolve dependencies when there was an invalid syntax in the module #[test] fn lsp_hover_deps_preserved_when_invalid_parse() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file1.ts", "languageId": "typescript", "version": 1, "text": "export type Foo = { bar(): string };\n" } })); client.did_open(json!({ "textDocument": { "uri": "file:///a/file2.ts", "languageId": "typescript", "version": 1, "text": "import { Foo } from './file1.ts'; declare const f: Foo; f\n" } })); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file2.ts" }, "position": { "line": 0, "character": 56 } }), ); assert_eq!( res, json!({ "contents": [ { "language": "typescript", "value": "const f: Foo", }, "" ], "range": { "start": { "line": 0, "character": 56, }, "end": { "line": 0, "character": 57, } } }) ); client.write_notification( "textDocument/didChange", json!({ "textDocument": { "uri": "file:///a/file2.ts", "version": 2 }, "contentChanges": [ { "range": { "start": { "line": 0, "character": 57 }, "end": { "line": 0, "character": 58 } }, "text": "." } ] }), ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file2.ts" }, "position": { "line": 0, "character": 56 } }), ); assert_eq!( res, json!({ "contents": [ { "language": "typescript", "value": "const f: Foo", }, "" ], "range": { "start": { "line": 0, "character": 56, }, "end": { "line": 0, "character": 57, } } }) ); client.shutdown(); } #[test] fn lsp_hover_typescript_types() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "import * as a from \"http://127.0.0.1:4545/xTypeScriptTypes.js\";\n\nconsole.log(a.foo);\n", } }), ); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [ ["http://127.0.0.1:4545/xTypeScriptTypes.js"], "file:///a/file.ts", ], }), ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 0, "character": 24 } }), ); assert_eq!( res, json!({ "contents": { "kind": "markdown", "value": "**Resolved Dependency**\n\n**Code**: http​://127.0.0.1:4545/xTypeScriptTypes.js\n\n**Types**: http​://127.0.0.1:4545/xTypeScriptTypes.d.ts\n" }, "range": { "start": { "line": 0, "character": 19 }, "end": { "line": 0, "character": 62 } } }) ); client.shutdown(); } #[test] fn lsp_hover_jsdoc_symbol_link() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/b.ts", "languageId": "typescript", "version": 1, "text": "export function hello() {}\n" } })); client.did_open( json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "import { hello } from \"./b.ts\";\n\nhello();\n\nconst b = \"b\";\n\n/** JSDoc {@link hello} and {@linkcode b} */\nfunction a() {}\n" } }), ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 7, "character": 10 } }), ); assert_eq!( res, json!({ "contents": [ { "language": "typescript", "value": "function a(): void" }, "JSDoc [hello](file:///a/file.ts#L1,10) and [`b`](file:///a/file.ts#L5,7)" ], "range": { "start": { "line": 7, "character": 9 }, "end": { "line": 7, "character": 10 } } }) ); client.shutdown(); } #[test] fn lsp_goto_type_definition() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "interface A {\n a: string;\n}\n\nexport class B implements A {\n a = \"a\";\n log() {\n console.log(this.a);\n }\n}\n\nconst b = new B();\nb;\n", } }), ); let res = client.write_request( "textDocument/typeDefinition", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 12, "character": 1 } }), ); assert_eq!( res, json!([ { "targetUri": "file:///a/file.ts", "targetRange": { "start": { "line": 4, "character": 0 }, "end": { "line": 9, "character": 1 } }, "targetSelectionRange": { "start": { "line": 4, "character": 13 }, "end": { "line": 4, "character": 14 } } } ]) ); client.shutdown(); } #[test] fn lsp_call_hierarchy() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "function foo() {\n return false;\n}\n\nclass Bar {\n baz() {\n return foo();\n }\n}\n\nfunction main() {\n const bar = new Bar();\n bar.baz();\n}\n\nmain();" } }), ); let res = client.write_request( "textDocument/prepareCallHierarchy", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 5, "character": 3 } }), ); assert_eq!( res, json!([{ "name": "baz", "kind": 6, "detail": "Bar", "uri": "file:///a/file.ts", "range": { "start": { "line": 5, "character": 2 }, "end": { "line": 7, "character": 3 } }, "selectionRange": { "start": { "line": 5, "character": 2 }, "end": { "line": 5, "character": 5 } } }]) ); let res = client.write_request( "callHierarchy/incomingCalls", json!({ "item": { "name": "baz", "kind": 6, "detail": "Bar", "uri": "file:///a/file.ts", "range": { "start": { "line": 5, "character": 2 }, "end": { "line": 7, "character": 3 } }, "selectionRange": { "start": { "line": 5, "character": 2 }, "end": { "line": 5, "character": 5 } } } }), ); assert_eq!( res, json!([{ "from": { "name": "main", "kind": 12, "detail": "", "uri": "file:///a/file.ts", "range": { "start": { "line": 10, "character": 0 }, "end": { "line": 13, "character": 1 } }, "selectionRange": { "start": { "line": 10, "character": 9 }, "end": { "line": 10, "character": 13 } } }, "fromRanges": [ { "start": { "line": 12, "character": 6 }, "end": { "line": 12, "character": 9 } } ] }]) ); let res = client.write_request( "callHierarchy/outgoingCalls", json!({ "item": { "name": "baz", "kind": 6, "detail": "Bar", "uri": "file:///a/file.ts", "range": { "start": { "line": 5, "character": 2 }, "end": { "line": 7, "character": 3 } }, "selectionRange": { "start": { "line": 5, "character": 2 }, "end": { "line": 5, "character": 5 } } } }), ); assert_eq!( res, json!([{ "to": { "name": "foo", "kind": 12, "detail": "", "uri": "file:///a/file.ts", "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 2, "character": 1 } }, "selectionRange": { "start": { "line": 0, "character": 9 }, "end": { "line": 0, "character": 12 } } }, "fromRanges": [{ "start": { "line": 6, "character": 11 }, "end": { "line": 6, "character": 14 } }] }]) ); client.shutdown(); } #[test] fn lsp_large_doc_changes() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let large_file_text = fs::read_to_string(testdata_path().join("lsp").join("large_file.txt")) .unwrap(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "javascript", "version": 1, "text": large_file_text, } })); client.write_notification( "textDocument/didChange", json!({ "textDocument": { "uri": "file:///a/file.ts", "version": 2 }, "contentChanges": [ { "range": { "start": { "line": 444, "character": 11 }, "end": { "line": 444, "character": 14 } }, "text": "+++" } ] }), ); client.write_notification( "textDocument/didChange", json!({ "textDocument": { "uri": "file:///a/file.ts", "version": 2 }, "contentChanges": [ { "range": { "start": { "line": 445, "character": 4 }, "end": { "line": 445, "character": 4 } }, "text": "// " } ] }), ); client.write_notification( "textDocument/didChange", json!({ "textDocument": { "uri": "file:///a/file.ts", "version": 2 }, "contentChanges": [ { "range": { "start": { "line": 477, "character": 4 }, "end": { "line": 477, "character": 9 } }, "text": "error" } ] }), ); client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 421, "character": 30 } }), ); client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 444, "character": 6 } }), ); client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 461, "character": 34 } }), ); client.shutdown(); assert!(client.duration().as_millis() <= 15000); } #[test] fn lsp_document_symbol() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "interface IFoo {\n foo(): boolean;\n}\n\nclass Bar implements IFoo {\n constructor(public x: number) { }\n foo() { return true; }\n /** @deprecated */\n baz() { return false; }\n get value(): number { return 0; }\n set value(_newValue: number) { return; }\n static staticBar = new Bar(0);\n private static getStaticBar() { return Bar.staticBar; }\n}\n\nenum Values { value1, value2 }\n\nvar bar: IFoo = new Bar(3);" } }), ); let res = client.write_request( "textDocument/documentSymbol", json!({ "textDocument": { "uri": "file:///a/file.ts" } }), ); assert_eq!( res, json!([{ "name": "bar", "kind": 13, "range": { "start": { "line": 17, "character": 4 }, "end": { "line": 17, "character": 26 } }, "selectionRange": { "start": { "line": 17, "character": 4 }, "end": { "line": 17, "character": 7 } } }, { "name": "Bar", "kind": 5, "range": { "start": { "line": 4, "character": 0 }, "end": { "line": 13, "character": 1 } }, "selectionRange": { "start": { "line": 4, "character": 6 }, "end": { "line": 4, "character": 9 } }, "children": [{ "name": "constructor", "kind": 9, "range": { "start": { "line": 5, "character": 2 }, "end": { "line": 5, "character": 35 } }, "selectionRange": { "start": { "line": 5, "character": 2 }, "end": { "line": 5, "character": 35 } } }, { "name": "baz", "kind": 6, "tags": [1], "range": { "start": { "line": 8, "character": 2 }, "end": { "line": 8, "character": 25 } }, "selectionRange": { "start": { "line": 8, "character": 2 }, "end": { "line": 8, "character": 5 } } }, { "name": "foo", "kind": 6, "range": { "start": { "line": 6, "character": 2 }, "end": { "line": 6, "character": 24 } }, "selectionRange": { "start": { "line": 6, "character": 2 }, "end": { "line": 6, "character": 5 } } }, { "name": "getStaticBar", "kind": 6, "range": { "start": { "line": 12, "character": 2 }, "end": { "line": 12, "character": 57 } }, "selectionRange": { "start": { "line": 12, "character": 17 }, "end": { "line": 12, "character": 29 } } }, { "name": "staticBar", "kind": 8, "range": { "start": { "line": 11, "character": 2 }, "end": { "line": 11, "character": 32 } }, "selectionRange": { "start": { "line": 11, "character": 9 }, "end": { "line": 11, "character": 18 } } }, { "name": "(get) value", "kind": 8, "range": { "start": { "line": 9, "character": 2 }, "end": { "line": 9, "character": 35 } }, "selectionRange": { "start": { "line": 9, "character": 6 }, "end": { "line": 9, "character": 11 } } }, { "name": "(set) value", "kind": 8, "range": { "start": { "line": 10, "character": 2 }, "end": { "line": 10, "character": 42 } }, "selectionRange": { "start": { "line": 10, "character": 6 }, "end": { "line": 10, "character": 11 } } }, { "name": "x", "kind": 8, "range": { "start": { "line": 5, "character": 14 }, "end": { "line": 5, "character": 30 } }, "selectionRange": { "start": { "line": 5, "character": 21 }, "end": { "line": 5, "character": 22 } } }] }, { "name": "IFoo", "kind": 11, "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 2, "character": 1 } }, "selectionRange": { "start": { "line": 0, "character": 10 }, "end": { "line": 0, "character": 14 } }, "children": [{ "name": "foo", "kind": 6, "range": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 17 } }, "selectionRange": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 5 } } }] }, { "name": "Values", "kind": 10, "range": { "start": { "line": 15, "character": 0 }, "end": { "line": 15, "character": 30 } }, "selectionRange": { "start": { "line": 15, "character": 5 }, "end": { "line": 15, "character": 11 } }, "children": [{ "name": "value1", "kind": 22, "range": { "start": { "line": 15, "character": 14 }, "end": { "line": 15, "character": 20 } }, "selectionRange": { "start": { "line": 15, "character": 14 }, "end": { "line": 15, "character": 20 } } }, { "name": "value2", "kind": 22, "range": { "start": { "line": 15, "character": 22 }, "end": { "line": 15, "character": 28 } }, "selectionRange": { "start": { "line": 15, "character": 22 }, "end": { "line": 15, "character": 28 } } }] }] ) ); client.shutdown(); } #[test] fn lsp_folding_range() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "// #region 1\n/*\n * Some comment\n */\nclass Foo {\n bar(a, b) {\n if (a === b) {\n return true;\n }\n return false;\n }\n}\n// #endregion" } }), ); let res = client.write_request( "textDocument/foldingRange", json!({ "textDocument": { "uri": "file:///a/file.ts" } }), ); assert_eq!( res, json!([{ "startLine": 0, "endLine": 12, "kind": "region" }, { "startLine": 1, "endLine": 3, "kind": "comment" }, { "startLine": 4, "endLine": 10 }, { "startLine": 5, "endLine": 9 }, { "startLine": 6, "endLine": 7 }]) ); client.shutdown(); } #[test] fn lsp_rename() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, // this should not rename in comments and strings "text": "let variable = 'a'; // variable\nconsole.log(variable);\n\"variable\";\n" } }), ); let res = client.write_request( "textDocument/rename", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 0, "character": 4 }, "newName": "variable_modified" }), ); assert_eq!( res, json!({ "documentChanges": [{ "textDocument": { "uri": "file:///a/file.ts", "version": 1 }, "edits": [{ "range": { "start": { "line": 0, "character": 4 }, "end": { "line": 0, "character": 12 } }, "newText": "variable_modified" }, { "range": { "start": { "line": 1, "character": 12 }, "end": { "line": 1, "character": 20 } }, "newText": "variable_modified" }] }] }) ); client.shutdown(); } #[test] fn lsp_selection_range() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "class Foo {\n bar(a, b) {\n if (a === b) {\n return true;\n }\n return false;\n }\n}" } }), ); let res = client.write_request( "textDocument/selectionRange", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "positions": [{ "line": 2, "character": 8 }] }), ); assert_eq!( res, json!([{ "range": { "start": { "line": 2, "character": 8 }, "end": { "line": 2, "character": 9 } }, "parent": { "range": { "start": { "line": 2, "character": 8 }, "end": { "line": 2, "character": 15 } }, "parent": { "range": { "start": { "line": 2, "character": 4 }, "end": { "line": 4, "character": 5 } }, "parent": { "range": { "start": { "line": 1, "character": 13 }, "end": { "line": 6, "character": 2 } }, "parent": { "range": { "start": { "line": 1, "character": 12 }, "end": { "line": 6, "character": 3 } }, "parent": { "range": { "start": { "line": 1, "character": 2 }, "end": { "line": 6, "character": 3 } }, "parent": { "range": { "start": { "line": 0, "character": 11 }, "end": { "line": 7, "character": 0 } }, "parent": { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 7, "character": 1 } } } } } } } } } }]) ); client.shutdown(); } #[test] fn lsp_semantic_tokens() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "enum Values { value1, value2 }\n\nasync function baz(s: string): Promise {\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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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::>( "textDocument/codeLens", json!({ "textDocument": { "uri": "deno:/asset/lib.deno.shared_globals.d.ts" } }), ); assert!(res.len() > 50); client.write_request_with_res_as::( "codeLens/resolve", json!({ "range": { "start": { "line": 416, "character": 12 }, "end": { "line": 416, "character": 19 } }, "data": { "specifier": "asset:///lib.deno.shared_globals.d.ts", "source": "references" } }), ); client.shutdown(); } #[test] fn lsp_nav_tree_updates() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.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" }] }] } }, { "title": "Add all missing 'async' modifiers", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 7 } }, "severity": 1, "code": 1308, "source": "deno-ts", "message": "'await' expressions are only allowed within async functions and at the top levels of modules.", "relatedInformation": [] }], "data": { "specifier": "file:///a/file.ts", "fixId": "fixAwaitInSyncFunction" } }]) ); let res = client .write_request( "codeAction/resolve", json!({ "title": "Add all missing 'async' modifiers", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 7 } }, "severity": 1, "code": 1308, "source": "deno-ts", "message": "'await' expressions are only allowed within async functions and at the top levels of modules.", "relatedInformation": [] }], "data": { "specifier": "file:///a/file.ts", "fixId": "fixAwaitInSyncFunction" } }), ) ; assert_eq!( res, json!({ "title": "Add all missing 'async' modifiers", "kind": "quickfix", "diagnostics": [ { "range": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 7 } }, "severity": 1, "code": 1308, "source": "deno-ts", "message": "'await' expressions are only allowed within async functions and at the top levels of modules.", "relatedInformation": [] } ], "edit": { "documentChanges": [{ "textDocument": { "uri": "file:///a/file.ts", "version": 1 }, "edits": [{ "range": { "start": { "line": 0, "character": 7 }, "end": { "line": 0, "character": 7 } }, "newText": "async " }, { "range": { "start": { "line": 0, "character": 21 }, "end": { "line": 0, "character": 25 } }, "newText": "Promise" }, { "range": { "start": { "line": 4, "character": 7 }, "end": { "line": 4, "character": 7 } }, "newText": "async " }, { "range": { "start": { "line": 4, "character": 21 }, "end": { "line": 4, "character": 25 } }, "newText": "Promise" }] }] }, "data": { "specifier": "file:///a/file.ts", "fixId": "fixAwaitInSyncFunction" } }) ); client.shutdown(); } #[test] fn test_lsp_code_actions_ordering() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": r#" import "https://deno.land/x/a/mod.ts"; let a = "a"; console.log(a); export function b(): void { await Promise.resolve("b"); } "# } })); let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "range": { "start": { "line": 1, "character": 11 }, "end": { "line": 6, "character": 12 } }, "context": { "diagnostics": diagnostics.all(), "only": ["quickfix"] } }), ); // Simplify the serialization to `{ title, source }` for this test. let mut actions: Vec = serde_json::from_value(res).unwrap(); for action in &mut actions { let action = action.as_object_mut().unwrap(); let title = action.get("title").unwrap().as_str().unwrap().to_string(); let diagnostics = action.get("diagnostics").unwrap().as_array().unwrap(); let diagnostic = diagnostics.first().unwrap().as_object().unwrap(); let source = diagnostic.get("source").unwrap(); let source = source.as_str().unwrap().to_string(); action.clear(); action.insert("title".to_string(), serde_json::to_value(title).unwrap()); action.insert("source".to_string(), serde_json::to_value(source).unwrap()); } let res = serde_json::to_value(actions).unwrap(); // Ensure ordering is "deno-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_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(); } #[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\" | \"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", "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 {printHello} from 'print_hello';\n", "\n", ), } }), ); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [ [ "npm:@denotest/types-exports-subpaths@1/client", "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" }] }] } }]) ); } #[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 >(); assert_eq!( json!(non_existent_diagnostics), json!([ { "range": { "start": { "line": 0, "character": 15 }, "end": { "line": 0, "character": 34 }, }, "severity": 1, "code": "resolver-error", "source": "deno", "message": "Unknown Node built-in module: non-existent" } ]) ); // update to have fs import client.write_notification( "textDocument/didChange", json!({ "textDocument": { "uri": "file:///a/file.ts", "version": 2 }, "contentChanges": [ { "range": { "start": { "line": 0, "character": 16 }, "end": { "line": 0, "character": 33 }, }, "text": "fs" } ] }), ); let diagnostics = client.read_diagnostics(); let diagnostics = diagnostics .messages_with_file_and_source("file:///a/file.ts", "deno") .diagnostics .into_iter() .filter(|d| { d.code == Some(lsp::NumberOrString::String( "import-node-prefix-missing".to_string(), )) }) .collect::>(); // get the quick fixes let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "range": { "start": { "line": 0, "character": 16 }, "end": { "line": 0, "character": 18 }, }, "context": { "diagnostics": json!(diagnostics), "only": ["quickfix"] } }), ); assert_eq!( res, json!([{ "title": "Update specifier to node:fs", "kind": "quickfix", "diagnostics": [ { "range": { "start": { "line": 0, "character": 15 }, "end": { "line": 0, "character": 19 } }, "severity": 1, "code": "import-node-prefix-missing", "source": "deno", "message": "Relative import path \"fs\" not prefixed with / or ./ or ../\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::>(); assert_eq!( json!(cache_diagnostics), json!([ { "range": { "start": { "line": 0, "character": 15 }, "end": { "line": 0, "character": 24 } }, "data": { "specifier": "npm:@types/node", }, "severity": 1, "code": "no-cache-npm", "source": "deno", "message": "Uncached or missing npm package: @types/node" } ]) ); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [["npm:@types/node"], "file:///a/file.ts"], }), ); client.write_notification( "textDocument/didChange", json!({ "textDocument": { "uri": "file:///a/file.ts", "version": 4 }, "contentChanges": [ { "range": { "start": { "line": 2, "character": 0 }, "end": { "line": 2, "character": 0 } }, "text": "fs." } ] }), ); client.read_diagnostics(); let list = client.get_completion_list( "file:///a/file.ts", (2, 3), json!({ "triggerKind": 2, "triggerCharacter": "." }), ); assert!(!list.is_incomplete); assert!(list.items.iter().any(|i| i.label == "writeFile")); assert!(list.items.iter().any(|i| i.label == "writeFileSync")); client.shutdown(); } #[test] fn lsp_completions_registry() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.add_test_server_suggestions(); }); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "import * as a from \"http://localhost:4545/x/a@\"" } })); let list = client.get_completion_list( "file:///a/file.ts", (0, 46), json!({ "triggerKind": 2, "triggerCharacter": "@" }), ); assert!(!list.is_incomplete); assert_eq!(list.items.len(), 3); let res = client.write_request( "completionItem/resolve", json!({ "label": "v2.0.0", "kind": 19, "detail": "(version)", "sortText": "0000000003", "filterText": "http://localhost:4545/x/a@v2.0.0", "textEdit": { "range": { "start": { "line": 0, "character": 20 }, "end": { "line": 0, "character": 46 } }, "newText": "http://localhost:4545/x/a@v2.0.0" } }), ); assert_eq!( res, json!({ "label": "v2.0.0", "kind": 19, "detail": "(version)", "sortText": "0000000003", "filterText": "http://localhost:4545/x/a@v2.0.0", "textEdit": { "range": { "start": { "line": 0, "character": 20 }, "end": { "line": 0, "character": 46 } }, "newText": "http://localhost:4545/x/a@v2.0.0" } }) ); client.shutdown(); } #[test] fn lsp_completions_registry_empty() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.add_test_server_suggestions(); }); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "import * as a from \"\"" } })); let res = client.get_completion( "file:///a/file.ts", (0, 20), json!({ "triggerKind": 2, "triggerCharacter": "\"" }), ); assert_eq!( json!(res), json!({ "isIncomplete": false, "items": [{ "label": ".", "kind": 19, "detail": "(local)", "sortText": "1", "insertText": ".", "commitCharacters": ["\"", "'"] }, { "label": "..", "kind": 19, "detail": "(local)", "sortText": "1", "insertText": "..", "commitCharacters": ["\"", "'" ] }, { "label": "http://localhost:4545", "kind": 19, "detail": "(registry)", "sortText": "2", "textEdit": { "range": { "start": { "line": 0, "character": 20 }, "end": { "line": 0, "character": 20 } }, "newText": "http://localhost:4545" }, "commitCharacters": ["\"", "'"] }] }) ); client.shutdown(); } #[test] fn lsp_auto_discover_registry() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "import * as a from \"http://localhost:4545/x/a@\"" } })); client.get_completion( "file:///a/file.ts", (0, 46), json!({ "triggerKind": 2, "triggerCharacter": "@" }), ); let (method, res) = client.read_notification(); assert_eq!(method, "deno/registryState"); assert_eq!( res, Some(json!({ "origin": "http://localhost:4545", "suggestions": true, })) ); client.shutdown(); } #[test] fn lsp_cache_location() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_cache(".cache").add_test_server_suggestions(); }); client.did_open(json!({ "textDocument": { "uri": "file:///a/file_01.ts", "languageId": "typescript", "version": 1, "text": "export const a = \"a\";\n", } })); let diagnostics = client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "import * as a from \"http://127.0.0.1:4545/xTypeScriptTypes.js\";\n// @deno-types=\"http://127.0.0.1:4545/type_definitions/foo.d.ts\"\nimport * as b from \"http://127.0.0.1:4545/type_definitions/foo.js\";\nimport * as c from \"http://127.0.0.1:4545/subdir/type_reference.js\";\nimport * as d from \"http://127.0.0.1:4545/subdir/mod1.ts\";\nimport * as e from \"data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=\";\nimport * as f from \"./file_01.ts\";\nimport * as g from \"http://localhost:4545/x/a/mod.ts\";\n\nconsole.log(a, b, c, d, e, f, g);\n" } })); assert_eq!(diagnostics.all().len(), 6); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], "file:///a/file.ts"], }), ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "position": { "line": 0, "character": 28 } }), ); assert_eq!( res, json!({ "contents": { "kind": "markdown", "value": "**Resolved Dependency**\n\n**Code**: http​://127.0.0.1:4545/xTypeScriptTypes.js\n\n**Types**: http​://127.0.0.1:4545/xTypeScriptTypes.d.ts\n" }, "range": { "start": { "line": 0, "character": 19 }, "end": { "line": 0, "character": 62 } } }) ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "position": { "line": 7, "character": 28 } }), ); assert_eq!( res, json!({ "contents": { "kind": "markdown", "value": "**Resolved Dependency**\n\n**Code**: http​://localhost:4545/x/a/mod.ts\n\n\n---\n\n**a**\n\nmod.ts" }, "range": { "start": { "line": 7, "character": 19 }, "end": { "line": 7, "character": 53 } } }) ); let cache_path = temp_dir.path().join(".cache"); assert!(cache_path.is_dir()); assert!(!cache_path.join("gen").is_dir()); // not created because no emitting has occurred client.shutdown(); } /// Sets the TLS root certificate on startup, which allows the LSP to connect to /// the custom signed test server and be able to retrieve the registry config /// and cache files. #[test] fn lsp_tls_cert() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder .set_suggest_imports_hosts(vec![ ("http://localhost:4545/".to_string(), true), ("https://localhost:5545/".to_string(), true), ]) .set_tls_certificate(""); }); client.did_open(json!({ "textDocument": { "uri": "file:///a/file_01.ts", "languageId": "typescript", "version": 1, "text": "export const a = \"a\";\n", } })); let diagnostics = client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "import * as a from \"https://localhost:5545/xTypeScriptTypes.js\";\n// @deno-types=\"https://localhost:5545/type_definitions/foo.d.ts\"\nimport * as b from \"https://localhost:5545/type_definitions/foo.js\";\nimport * as c from \"https://localhost:5545/subdir/type_reference.js\";\nimport * as d from \"https://localhost:5545/subdir/mod1.ts\";\nimport * as e from \"data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=\";\nimport * as f from \"./file_01.ts\";\nimport * as g from \"http://localhost:4545/x/a/mod.ts\";\n\nconsole.log(a, b, c, d, e, f, g);\n" } })); let diagnostics = diagnostics.all(); assert_eq!(diagnostics.len(), 6); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [[], "file:///a/file.ts"], }), ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "position": { "line": 0, "character": 28 } }), ); assert_eq!( res, json!({ "contents": { "kind": "markdown", "value": "**Resolved Dependency**\n\n**Code**: https​://localhost:5545/xTypeScriptTypes.js\n" }, "range": { "start": { "line": 0, "character": 19 }, "end": { "line": 0, "character": 63 } } }) ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts", }, "position": { "line": 7, "character": 28 } }), ); assert_eq!( res, json!({ "contents": { "kind": "markdown", "value": "**Resolved Dependency**\n\n**Code**: http​://localhost:4545/x/a/mod.ts\n\n\n---\n\n**a**\n\nmod.ts" }, "range": { "start": { "line": 7, "character": 19 }, "end": { "line": 7, "character": 53 } } }) ); client.shutdown(); } #[test] fn lsp_diagnostics_warn_redirect() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "import * as a from \"http://127.0.0.1:4545/x_deno_warning.js\";\n\nconsole.log(a)\n", }, }), ); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [ ["http://127.0.0.1:4545/x_deno_warning.js"], "file:///a/file.ts", ], }), ); let diagnostics = client.read_diagnostics(); assert_eq!( diagnostics.messages_with_source("deno"), lsp::PublishDiagnosticsParams { uri: Url::parse("file:///a/file.ts").unwrap(), diagnostics: vec![ lsp::Diagnostic { range: lsp::Range { start: lsp::Position { line: 0, character: 19 }, end: lsp::Position { line: 0, character: 60 } }, severity: Some(lsp::DiagnosticSeverity::WARNING), code: Some(lsp::NumberOrString::String("deno-warn".to_string())), source: Some("deno".to_string()), message: "foobar".to_string(), ..Default::default() }, lsp::Diagnostic { range: lsp::Range { start: lsp::Position { line: 0, character: 19 }, end: lsp::Position { line: 0, character: 60 } }, severity: Some(lsp::DiagnosticSeverity::INFORMATION), code: Some(lsp::NumberOrString::String("redirect".to_string())), source: Some("deno".to_string()), message: "The import of \"http://127.0.0.1:4545/x_deno_warning.js\" was redirected to \"http://127.0.0.1:4545/lsp/x_deno_warning_redirect.js\".".to_string(), data: Some(json!({"specifier": "http://127.0.0.1:4545/x_deno_warning.js", "redirect": "http://127.0.0.1:4545/lsp/x_deno_warning_redirect.js"})), ..Default::default() } ], version: Some(1), } ); client.shutdown(); } #[test] fn lsp_redirect_quick_fix() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open( json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "import * as a from \"http://127.0.0.1:4545/x_deno_warning.js\";\n\nconsole.log(a)\n", }, }), ); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [ ["http://127.0.0.1:4545/x_deno_warning.js"], "file:///a/file.ts", ], }), ); let diagnostics = client .read_diagnostics() .messages_with_source("deno") .diagnostics; let res = client.write_request( "textDocument/codeAction", json!(json!({ "textDocument": { "uri": "file:///a/file.ts" }, "range": { "start": { "line": 0, "character": 19 }, "end": { "line": 0, "character": 60 } }, "context": { "diagnostics": diagnostics, "only": ["quickfix"] } })), ); assert_eq!( res, json!([{ "title": "Update specifier to its redirected specifier.", "kind": "quickfix", "diagnostics": [ { "range": { "start": { "line": 0, "character": 19 }, "end": { "line": 0, "character": 60 } }, "severity": 3, "code": "redirect", "source": "deno", "message": "The import of \"http://127.0.0.1:4545/x_deno_warning.js\" was redirected to \"http://127.0.0.1:4545/lsp/x_deno_warning_redirect.js\".", "data": { "specifier": "http://127.0.0.1:4545/x_deno_warning.js", "redirect": "http://127.0.0.1:4545/lsp/x_deno_warning_redirect.js" } } ], "edit": { "changes": { "file:///a/file.ts": [ { "range": { "start": { "line": 0, "character": 19 }, "end": { "line": 0, "character": 60 } }, "newText": "\"http://127.0.0.1:4545/lsp/x_deno_warning_redirect.js\"" } ] } } }]) ); client.shutdown(); } #[test] fn lsp_diagnostics_deprecated() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "/** @deprecated */\nexport const a = \"a\";\n\na;\n", }, })); assert_eq!( json!(diagnostics.all_messages()), json!([{ "uri": "file:///a/file.ts", "diagnostics": [ { "range": { "start": { "line": 3, "character": 0 }, "end": { "line": 3, "character": 1 } }, "severity": 4, "code": 6385, "source": "deno-ts", "message": "'a' is deprecated.", "relatedInformation": [ { "location": { "uri": "file:///a/file.ts", "range": { "start": { "line": 0, "character": 4, }, "end": { "line": 0, "character": 16, }, }, }, "message": "The declaration was marked as deprecated here.", }, ], "tags": [2] } ], "version": 1 }]) ); client.shutdown(); } #[test] fn lsp_diagnostics_deno_types() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client .did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "/// \n/// , } #[test] fn lsp_performance() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "console.log(Deno.args);\n" } })); client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "position": { "line": 0, "character": 19 } }), ); let res = client.write_request_with_res_as::( "deno/performance", json!(null), ); let mut averages = res .averages .iter() .map(|a| a.name.as_str()) .collect::>(); averages.sort(); assert_eq!( averages, vec![ "lsp.did_open", "lsp.hover", "lsp.initialize", "lsp.testing_update", "lsp.update_cache", "lsp.update_diagnostics_deps", "lsp.update_diagnostics_lint", "lsp.update_diagnostics_ts", "lsp.update_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 mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { // Also test out using a non-json file extension here. // What should matter is the language identifier. "uri": "file:///a/file.lock", "languageId": "json", "version": 1, "text": "{\"key\":\"value\"}" } })); let res = client.write_request( "textDocument/formatting", json!({ "textDocument": { "uri": "file:///a/file.lock" }, "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 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/formatting", json!({ "textDocument": { "uri": "file:///a/file.md" }, "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"); }); client .did_open( json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "export async function someVeryLongFunctionName() {\nconst response = fetch(\"http://localhost:4545/some/non/existent/path.json\");\nconsole.log(response.text());\nconsole.log(\"finished!\")\n}" } }), ); // The options below should be ignored in favor of configuration from config file. 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": 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.map(callbackfn: (value: never, index: number, array: never[]) => U, thisArg?: any): U[]", "documentation": { "kind": "markdown", "value": "Calls a defined callback function on each element of an array, and returns an array that contains the results.\n\n*@param* - callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.*@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(callbackfn)", "insertTextFormat": 1 }) ); 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 res = client.write_request( "workspace/symbol", json!({ "query": "field" }), ); assert_eq!( res, json!([{ "name": "fieldA", "kind": 8, "location": { "uri": "file:///a/file.ts", "range": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 17 } } }, "containerName": "A" }, { "name": "fieldB", "kind": 8, "location": { "uri": "file:///a/file.ts", "range": { "start": { "line": 2, "character": 2 }, "end": { "line": 2, "character": 17 } } }, "containerName": "A" }, { "name": "fieldC", "kind": 8, "location": { "uri": "file:///a/file_01.ts", "range": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 17 } } }, "containerName": "B" }, { "name": "fieldD", "kind": 8, "location": { "uri": "file:///a/file_01.ts", "range": { "start": { "line": 2, "character": 2 }, "end": { "line": 2, "character": 17 } } }, "containerName": "B" }, { "name": "ClassFieldDecoratorContext", "kind": 11, "location": { "uri": "deno:/asset/lib.decorators.d.ts", "range": { "start": { "line": 343, "character": 0, }, "end": { "line": 385, "character": 1, }, }, }, "containerName": "", }]) ); client.shutdown(); } #[test] fn lsp_code_actions_ignore_lint() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "let message = 'Hello, Deno!';\nconsole.log(message);\n" } })); let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "range": { "start": { "line": 1, "character": 5 }, "end": { "line": 1, "character": 12 } }, "context": { "diagnostics": [ { "range": { "start": { "line": 1, "character": 5 }, "end": { "line": 1, "character": 12 } }, "severity": 1, "code": "prefer-const", "source": "deno-lint", "message": "'message' is never reassigned\nUse 'const' instead", "relatedInformation": [] } ], "only": ["quickfix"] } }), ); assert_eq!( res, json!([{ "title": "Disable prefer-const for this line", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 1, "character": 5 }, "end": { "line": 1, "character": 12 } }, "severity": 1, "code": "prefer-const", "source": "deno-lint", "message": "'message' is never reassigned\nUse 'const' instead", "relatedInformation": [] }], "edit": { "changes": { "file:///a/file.ts": [{ "range": { "start": { "line": 1, "character": 0 }, "end": { "line": 1, "character": 0 } }, "newText": "// deno-lint-ignore prefer-const\n" }] } } }, { "title": "Disable prefer-const for the entire file", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 1, "character": 5 }, "end": { "line": 1, "character": 12 } }, "severity": 1, "code": "prefer-const", "source": "deno-lint", "message": "'message' is never reassigned\nUse 'const' instead", "relatedInformation": [] }], "edit": { "changes": { "file:///a/file.ts": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "// deno-lint-ignore-file prefer-const\n" }] } } }, { "title": "Ignore lint errors for the entire file", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 1, "character": 5 }, "end": { "line": 1, "character": 12 } }, "severity": 1, "code": "prefer-const", "source": "deno-lint", "message": "'message' is never reassigned\nUse 'const' instead", "relatedInformation": [] }], "edit": { "changes": { "file:///a/file.ts": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "// deno-lint-ignore-file\n" }] } } }]) ); client.shutdown(); } /// This test exercises updating an existing deno-lint-ignore-file comment. #[test] fn lsp_code_actions_update_ignore_lint() { let context = TestContextBuilder::new().use_temp_cwd().build(); let mut client = context.new_lsp_command().build(); client.initialize_default(); client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "typescript", "version": 1, "text": "#!/usr/bin/env -S deno run // deno-lint-ignore-file camelcase let snake_case = 'Hello, Deno!'; console.log(snake_case); ", } })); let res = client.write_request( "textDocument/codeAction", json!({ "textDocument": { "uri": "file:///a/file.ts" }, "range": { "start": { "line": 3, "character": 5 }, "end": { "line": 3, "character": 15 } }, "context": { "diagnostics": [{ "range": { "start": { "line": 3, "character": 5 }, "end": { "line": 3, "character": 15 } }, "severity": 1, "code": "prefer-const", "source": "deno-lint", "message": "'snake_case' is never reassigned\nUse 'const' instead", "relatedInformation": [] }], "only": ["quickfix"] } }), ); assert_eq!( res, json!([{ "title": "Disable prefer-const for this line", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 3, "character": 5 }, "end": { "line": 3, "character": 15 } }, "severity": 1, "code": "prefer-const", "source": "deno-lint", "message": "'snake_case' is never reassigned\nUse 'const' instead", "relatedInformation": [] }], "edit": { "changes": { "file:///a/file.ts": [{ "range": { "start": { "line": 3, "character": 0 }, "end": { "line": 3, "character": 0 } }, "newText": "// deno-lint-ignore prefer-const\n" }] } } }, { "title": "Disable prefer-const for the entire file", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 3, "character": 5 }, "end": { "line": 3, "character": 15 } }, "severity": 1, "code": "prefer-const", "source": "deno-lint", "message": "'snake_case' is never reassigned\nUse 'const' instead", "relatedInformation": [] }], "edit": { "changes": { "file:///a/file.ts": [{ "range": { "start": { "line": 1, "character": 34 }, "end": { "line": 1, "character": 34 } }, "newText": " prefer-const" }] } } }, { "title": "Ignore lint errors for the entire file", "kind": "quickfix", "diagnostics": [{ "range": { "start": { "line": 3, "character": 5 }, "end": { "line": 3, "character": 15 } }, "severity": 1, "code": "prefer-const", "source": "deno-lint", "message": "'snake_case' is never reassigned\nUse 'const' instead", "relatedInformation": [] }], "edit": { "changes": { "file:///a/file.ts": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "newText": "// deno-lint-ignore-file\n" }] } } }]) ); client.shutdown(); } #[test] fn lsp_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 ; } ", } })); client.write_request( "workspace/executeCommand", json!({ "command": "deno.cache", "arguments": [ ["http://127.0.0.1:4545/jsx/jsx-runtime"], "file:///a/file.tsx", ], }), ); let res = client.write_request( "textDocument/hover", json!({ "textDocument": { "uri": "file:///a/file.tsx" }, "position": { "line": 0, "character": 25 } }), ); assert_eq!( res, json!({ "contents": { "kind": "markdown", "value": "**Resolved Dependency**\n\n**Code**: http​://localhost:4545/jsx/jsx-runtime\n", }, "range": { "start": { "line": 0, "character": 21 }, "end": { "line": 0, "character": 46 } } }) ); client.shutdown(); } #[ignore = "https://github.com/denoland/deno/issues/21770"] #[test] fn lsp_jsx_import_source_config_file_automatic_cache() { let context = TestContextBuilder::new() .use_http_server() .use_temp_cwd() .build(); let temp_dir = context.temp_dir(); temp_dir.write( "deno.json", json!({ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "http://localhost:4545/jsx", }, }) .to_string(), ); let mut client = context.new_lsp_command().build(); client.initialize_default(); let mut diagnostics = client.did_open(json!({ "textDocument": { "uri": temp_dir.uri().join("file.tsx").unwrap(), "languageId": "typescriptreact", "version": 1, "text": " export function Foo() { return
; } ", }, })); // 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
; } ", }, })); } assert_eq!(diagnostics.all(), vec![]); client.shutdown(); } #[derive(Debug, Clone, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] struct TestData { id: String, label: String, steps: Option>, range: Option, } #[derive(Debug, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] enum TestModuleNotificationKind { Insert, Replace, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct TestModuleNotificationParams { text_document: lsp::TextDocumentIdentifier, kind: TestModuleNotificationKind, label: String, tests: Vec, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct EnqueuedTestModule { text_document: lsp::TextDocumentIdentifier, ids: Vec, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct TestRunResponseParams { enqueued: Vec, } #[test] fn lsp_testing_api() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); let contents = r#" Deno.test({ name: "test a", async fn(t) { console.log("test a"); await t.step("step of test a", () => {}); } }); "#; 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::("deno/testModule"); let params: TestModuleNotificationParams = serde_json::from_value(notification.unwrap()).unwrap(); assert_eq!(params.text_document.uri, specifier); assert_eq!(params.kind, TestModuleNotificationKind::Replace); assert_eq!(params.label, "test.ts"); assert_eq!(params.tests.len(), 1); let test = ¶ms.tests[0]; assert_eq!(test.label, "test a"); assert_eq!( test.range, Some(lsp::Range { start: lsp::Position { line: 1, character: 5, }, end: lsp::Position { line: 1, character: 9, } }) ); let steps = test.steps.as_ref().unwrap(); assert_eq!(steps.len(), 1); let step = &steps[0]; assert_eq!(step.label, "step of test a"); assert_eq!( step.range, Some(lsp::Range { start: lsp::Position { line: 5, character: 12, }, end: lsp::Position { line: 5, character: 16, } }) ); let res = client.write_request_with_res_as::( "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 (method, notification) = client.read_notification::(); assert_eq!(method, "deno/testRunProgress"); assert_eq!( notification, Some(json!({ "id": 1, "message": { "type": "started", "test": { "textDocument": { "uri": specifier, }, "id": id, }, } })) ); let (method, notification) = client.read_notification::(); assert_eq!(method, "deno/testRunProgress"); let notification_value = notification .as_ref() .unwrap() .as_object() .unwrap() .get("message") .unwrap() .as_object() .unwrap() .get("value") .unwrap() .as_str() .unwrap(); // deno test's output capturing flushes with a zero-width space in order to // synchronize the output pipes. 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 (method, notification) = client.read_notification::(); assert_eq!(method, "deno/testRunProgress"); assert_eq!( notification, Some(json!({ "id": 1, "message": { "type": "started", "test": { "textDocument": { "uri": specifier, }, "id": id, "stepId": step.id, }, } })) ); let (method, notification) = client.read_notification::(); assert_eq!(method, "deno/testRunProgress"); let mut notification = notification.unwrap(); let duration = notification .as_object_mut() .unwrap() .get_mut("message") .unwrap() .as_object_mut() .unwrap() .remove("duration"); assert!(duration.is_some()); assert_eq!( notification, json!({ "id": 1, "message": { "type": "passed", "test": { "textDocument": { "uri": specifier, }, "id": id, "stepId": step.id, }, } }) ); let (method, notification) = client.read_notification::(); assert_eq!(method, "deno/testRunProgress"); let notification = notification.unwrap(); let obj = notification.as_object().unwrap(); assert_eq!(obj.get("id"), Some(&json!(1))); let message = obj.get("message").unwrap().as_object().unwrap(); match message.get("type").and_then(|v| v.as_str()) { Some("passed") => { assert_eq!( message.get("test"), Some(&json!({ "textDocument": { "uri": specifier }, "id": id, })) ); assert!(message.contains_key("duration")); let (method, notification) = client.read_notification::(); assert_eq!(method, "deno/testRunProgress"); assert_eq!( notification, Some(json!({ "id": 1, "message": { "type": "end", } })) ); } // sometimes on windows, the messages come out of order, but it actually is // working, so if we do get the end before the passed, we will simply let // the test pass Some("end") => (), _ => panic!("unexpected message {}", json!(notification)), } // 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 (method, notification) = client.read_notification::(); assert_eq!(method, "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::>(); // 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(); }