// Copyright 2018-2025 the Deno authors. MIT license.
use std::fs;
use std::str::FromStr;
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 test_util::assert_starts_with;
use test_util::assertions::assert_json_subset;
use test_util::deno_cmd_with_deno_dir;
use test_util::env_vars_for_npm_tests;
use test_util::lsp::range_of;
use test_util::lsp::source_file;
use test_util::lsp::LspClient;
use test_util::testdata_path;
use test_util::TestContextBuilder;
use tower_lsp::lsp_types as lsp;
#[test]
fn lsp_startup_shutdown() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.shutdown();
}
#[test]
fn lsp_init_tsconfig() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
temp_dir.write(
"lib.tsconfig.json",
r#"{
"compilerOptions": {
"lib": ["deno.ns", "deno.unstable", "dom"]
}
}"#,
);
let mut client = context.new_lsp_command().build();
client.initialize(|builder| {
builder.set_config("lib.tsconfig.json");
});
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "location.pathname;\n"
}
}));
assert_eq!(diagnostics.all().len(), 0);
client.shutdown();
}
#[test]
fn lsp_tsconfig_types() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
temp_dir.write(
"types.tsconfig.json",
r#"{
"compilerOptions": {
"types": ["./a.d.ts"]
},
"lint": {
"rules": {
"tags": []
}
}
}"#,
);
let a_dts = "// deno-lint-ignore-file no-var\ndeclare var a: string;";
temp_dir.write("a.d.ts", a_dts);
let mut client = context.new_lsp_command().build();
client.initialize(|builder| {
builder
.set_config("types.tsconfig.json")
// avoid finding the declaration file via the document preload
.set_preload_limit(0);
});
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("test.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": "console.log(a);\n"
}
}));
assert_eq!(json!(diagnostics.all()), json!([]));
client.shutdown();
}
#[test]
fn lsp_tsconfig_types_config_sub_dir() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
let sub_dir = temp_dir.path().join("sub_dir");
sub_dir.create_dir_all();
sub_dir.join("types.tsconfig.json").write(
r#"{
"compilerOptions": {
"types": ["./a.d.ts"]
},
"lint": {
"rules": {
"tags": []
}
}
}"#,
);
let a_dts = "// deno-lint-ignore-file no-var\ndeclare var a: string;";
sub_dir.join("a.d.ts").write(a_dts);
temp_dir.write("deno.json", "{}");
let mut client = context.new_lsp_command().build();
client.initialize(|builder| {
builder
.set_config("sub_dir/types.tsconfig.json")
// avoid finding the declaration file via the document preload
.set_preload_limit(0);
});
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("test.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": "console.log(a);\n"
}
}));
assert_eq!(json!(diagnostics.all()), json!([]));
client.shutdown();
}
#[test]
fn lsp_triple_slash_types() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
let a_dts = "// deno-lint-ignore-file no-var\ndeclare var a: string;";
temp_dir.write("a.d.ts", a_dts);
let mut client = context.new_lsp_command().build();
client.initialize_default();
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("test.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": "/// \n\nconsole.log(a);\n"
}
}));
assert_eq!(diagnostics.all().len(), 0);
client.shutdown();
}
#[test]
fn unadded_dependency_message_with_import_map() {
let context = TestContextBuilder::new()
.use_http_server()
.use_temp_cwd()
.build();
let temp_dir = context.temp_dir();
temp_dir.write(
"import_map.json",
json!({
"imports": {
}
})
.to_string(),
);
temp_dir.write(
"deno.json",
json!({
"importMap": "import_map.json".to_string(),
})
.to_string(),
);
temp_dir.write(
"file.ts",
r#"
import * as x from "@std/fs";
"#,
);
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.write_request(
"workspace/executeCommand",
json!({
"command": "deno.cache",
"arguments": [[], temp_dir.url().join("file.ts").unwrap()],
}),
);
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": temp_dir.read_to_string("file.ts"),
}
}));
// expected lsp_messages don't include the file path
let mut expected_lsp_messages = Vec::from(["`x` is never used\nIf this is intentional, prefix it with an underscore like `_x`",
"'x' is declared but its value is never read.",
"Relative import path \"@std/fs\" not prefixed with / or ./ or ../ and not in import map from \" Hint: Use [deno add @std/fs] to add the dependency."]);
expected_lsp_messages.sort();
let all_diagnostics = diagnostics.all();
let mut correct_lsp_messages = all_diagnostics
.iter()
.map(|d| d.message.as_str())
.collect::>();
correct_lsp_messages.sort();
let part1 = correct_lsp_messages[1].split("file").collect::>()[0];
let part2 = correct_lsp_messages[1].split('\n').collect::>()[1];
let file_path_removed_from_message = format!("{} {}", part1, part2);
correct_lsp_messages[1] = file_path_removed_from_message.as_str();
assert_eq!(correct_lsp_messages, expected_lsp_messages);
client.shutdown();
}
#[test]
fn unadded_dependency_message() {
let context = TestContextBuilder::new()
.use_http_server()
.use_temp_cwd()
.build();
let temp_dir = context.temp_dir();
temp_dir.write(
"deno.json",
json!({
"imports": {
}
})
.to_string(),
);
temp_dir.write(
"file.ts",
r#"
import * as x from "@std/fs";
"#,
);
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.write_request(
"workspace/executeCommand",
json!({
"command": "deno.cache",
"arguments": [[], temp_dir.url().join("file.ts").unwrap()],
}),
);
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": temp_dir.read_to_string("file.ts"),
}
}));
// expected lsp_messages don't include the file path
let mut expected_lsp_messages = Vec::from(["`x` is never used\nIf this is intentional, prefix it with an underscore like `_x`",
"'x' is declared but its value is never read.",
"Relative import path \"@std/fs\" not prefixed with / or ./ or ../ and not in import map from \" Hint: Use [deno add @std/fs] to add the dependency."]);
expected_lsp_messages.sort();
let all_diagnostics = diagnostics.all();
let mut correct_lsp_messages = all_diagnostics
.iter()
.map(|d| d.message.as_str())
.collect::>();
correct_lsp_messages.sort();
let part1 = correct_lsp_messages[1].split("file").collect::>()[0];
let part2 = correct_lsp_messages[1].split('\n').collect::>()[1];
let file_path_removed_from_message = format!("{} {}", part1, part2);
correct_lsp_messages[1] = file_path_removed_from_message.as_str();
assert_eq!(correct_lsp_messages, expected_lsp_messages);
client.shutdown();
}
#[test]
fn lsp_import_map() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
let import_map = r#"{
"imports": {
"/~/": "./lib/"
}
}"#;
temp_dir.write("import-map.json", import_map);
temp_dir.create_dir_all("lib");
temp_dir.write("lib/b.ts", r#"export const b = "b";"#);
let mut client = context.new_lsp_command().build();
client.initialize(|builder| {
builder.set_import_map("import-map.json");
});
let uri = temp_dir.url().join("a.ts").unwrap();
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": uri,
"languageId": "typescript",
"version": 1,
"text": "import { b } from \"/~/b.ts\";\n\nconsole.log(b);\n"
}
}));
assert_eq!(json!(diagnostics.all()), json!([]));
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": uri
},
"position": { "line": 2, "character": 12 }
}),
);
assert_eq!(
res,
json!({
"contents": [
{
"language": "typescript",
"value":"(alias) const b: \"b\"\nimport b"
},
""
],
"range": {
"start": { "line": 2, "character": 12 },
"end": { "line": 2, "character": 13 }
}
})
);
client.shutdown();
}
#[test]
fn lsp_import_map_remote() {
let context = TestContextBuilder::new()
.use_http_server()
.use_temp_cwd()
.build();
let temp_dir = context.temp_dir();
temp_dir.write(
"file.ts",
r#"
import { printHello } from "print_hello";
printHello();
"#,
);
let mut client = context.new_lsp_command().build();
client.initialize(|builder| {
builder.set_import_map(
"http://localhost:4545/import_maps/import_map_remote.json",
);
});
client.write_request(
"workspace/executeCommand",
json!({
"command": "deno.cache",
"arguments": [[], temp_dir.url().join("file.ts").unwrap()],
}),
);
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": temp_dir.read_to_string("file.ts"),
}
}));
assert_eq!(diagnostics.all(), vec![]);
client.shutdown();
}
#[test]
fn lsp_import_map_data_url() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
let mut client = context.new_lsp_command().build();
client.initialize(|builder| {
builder.set_import_map("data:application/json;utf8,{\"imports\": { \"example\": \"https://deno.land/x/example/mod.ts\" }}");
});
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": "import example from \"example\";\n"
}
}));
// This indicates that the import map is applied correctly.
assert!(diagnostics.all().iter().any(|diagnostic| diagnostic.code
== Some(lsp::NumberOrString::String("no-cache".to_string()))
&& diagnostic
.message
.contains("https://deno.land/x/example/mod.ts")));
client.shutdown();
}
#[test]
fn lsp_import_map_config_file() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
temp_dir.write(
"deno.import_map.jsonc",
r#"{
"importMap": "import-map.json"
}"#,
);
temp_dir.write(
"import-map.json",
r#"{
"imports": {
"/~/": "./lib/"
}
}"#,
);
temp_dir.create_dir_all("lib");
temp_dir.write("lib/b.ts", r#"export const b = "b";"#);
let mut client = context.new_lsp_command().build();
client.initialize(|builder| {
builder.set_config("./deno.import_map.jsonc");
});
let uri = temp_dir.url().join("a.ts").unwrap();
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": uri,
"languageId": "typescript",
"version": 1,
"text": "import { b } from \"/~/b.ts\";\n\nconsole.log(b);\n"
}
}));
assert_eq!(diagnostics.all().len(), 0);
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": uri
},
"position": { "line": 2, "character": 12 }
}),
);
assert_eq!(
res,
json!({
"contents": [
{
"language": "typescript",
"value":"(alias) const b: \"b\"\nimport b"
},
""
],
"range": {
"start": { "line": 2, "character": 12 },
"end": { "line": 2, "character": 13 }
}
})
);
client.shutdown();
}
#[test]
fn lsp_import_map_embedded_in_config_file() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
temp_dir.write(
"deno.embedded_import_map.jsonc",
r#"{
// some comment
"imports": {
"/~/": "./lib/"
}
}"#,
);
temp_dir.create_dir_all("lib");
temp_dir.write("lib/b.ts", r#"export const b = "b";"#);
let mut client = context.new_lsp_command().build();
client.initialize(|builder| {
builder.set_config("./deno.embedded_import_map.jsonc");
});
let uri = temp_dir.url().join("a.ts").unwrap();
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": uri,
"languageId": "typescript",
"version": 1,
"text": "import { b } from \"/~/b.ts\";\n\nconsole.log(b);\n"
}
}));
assert_eq!(diagnostics.all().len(), 0);
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": uri
},
"position": { "line": 2, "character": 12 }
}),
);
assert_eq!(
res,
json!({
"contents": [
{
"language": "typescript",
"value":"(alias) const b: \"b\"\nimport b"
},
""
],
"range": {
"start": { "line": 2, "character": 12 },
"end": { "line": 2, "character": 13 }
}
})
);
client.shutdown();
}
#[test]
fn lsp_import_map_embedded_in_config_file_after_initialize() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
temp_dir.write("deno.embedded_import_map.jsonc", "{}");
temp_dir.create_dir_all("lib");
temp_dir.write("lib/b.ts", r#"export const b = "b";"#);
let mut client = context.new_lsp_command().build();
client.initialize(|builder| {
builder.set_config("./deno.embedded_import_map.jsonc");
});
let uri = temp_dir.url().join("a.ts").unwrap();
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": uri,
"languageId": "typescript",
"version": 1,
"text": "import { b } from \"/~/b.ts\";\n\nconsole.log(b);\n"
}
}));
assert_eq!(diagnostics.all().len(), 1);
// update the import map
temp_dir.write(
"deno.embedded_import_map.jsonc",
r#"{
"imports": {
"/~/": "./lib/"
}
}"#,
);
client.did_change_watched_files(json!({
"changes": [{
"uri": temp_dir.url().join("deno.embedded_import_map.jsonc").unwrap(),
"type": 2
}]
}));
assert_eq!(json!(client.read_diagnostics().all()), json!([]));
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": uri
},
"position": { "line": 2, "character": 12 }
}),
);
assert_eq!(
res,
json!({
"contents": [
{
"language": "typescript",
"value":"(alias) const b: \"b\"\nimport b"
},
""
],
"range": {
"start": { "line": 2, "character": 12 },
"end": { "line": 2, "character": 13 }
}
})
);
client.shutdown();
}
#[test]
fn lsp_import_map_config_file_auto_discovered() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
temp_dir.create_dir_all("lib");
temp_dir.write("lib/b.ts", r#"export const b = "b";"#);
let mut client = context.new_lsp_command().capture_stderr().build();
client.initialize_default();
// add the deno.json
temp_dir.write("deno.jsonc", r#"{ "imports": { "/~/": "./lib/" } }"#);
client.did_change_watched_files(json!({
"changes": [{
"uri": temp_dir.url().join("deno.jsonc").unwrap(),
"type": 2
}]
}));
client.wait_until_stderr_line(|line| {
line.contains(" Resolved Deno configuration file:")
});
let uri = temp_dir.url().join("a.ts").unwrap();
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": uri,
"languageId": "typescript",
"version": 1,
"text": "import { b } from \"/~/b.ts\";\n\nconsole.log(b);\n"
}
}));
assert_eq!(diagnostics.all().len(), 0);
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": uri
},
"position": { "line": 2, "character": 12 }
}),
);
assert_eq!(
res,
json!({
"contents": [
{
"language": "typescript",
"value":"(alias) const b: \"b\"\nimport b"
},
""
],
"range": {
"start": { "line": 2, "character": 12 },
"end": { "line": 2, "character": 13 }
}
})
);
// now cause a syntax error
temp_dir.write("deno.jsonc", r#",,#,#,,"#);
client.did_change_watched_files(json!({
"changes": [{
"uri": temp_dir.url().join("deno.jsonc").unwrap(),
"type": 2
}]
}));
assert_eq!(client.read_diagnostics().all().len(), 1);
// now fix it, and things should work again
temp_dir.write("deno.jsonc", r#"{ "imports": { "/~/": "./lib/" } }"#);
client.did_change_watched_files(json!({
"changes": [{
"uri": temp_dir.url().join("deno.jsonc").unwrap(),
"type": 2
}]
}));
client.wait_until_stderr_line(|line| {
line.contains(" Resolved Deno configuration file:")
});
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": uri
},
"position": { "line": 2, "character": 12 }
}),
);
assert_eq!(
res,
json!({
"contents": [
{
"language": "typescript",
"value":"(alias) const b: \"b\"\nimport b"
},
""
],
"range": {
"start": { "line": 2, "character": 12 },
"end": { "line": 2, "character": 13 }
}
})
);
assert_eq!(client.read_diagnostics().all().len(), 0);
client.shutdown();
}
#[test]
fn lsp_import_map_config_file_auto_discovered_symlink() {
let context = TestContextBuilder::new()
// DO NOT COPY THIS CODE. Very rare case where we want to force the temp
// directory on the CI to not be a symlinked directory because we are
// testing a scenario with a symlink to a non-symlink in the same directory
// tree. Generally you want to ensure your code works in symlinked directories
// so don't use this unless you have a similar scenario.
.temp_dir_path(std::env::temp_dir().canonicalize().unwrap())
.use_temp_cwd()
.build();
let temp_dir = context.temp_dir();
temp_dir.create_dir_all("lib");
temp_dir.write("lib/b.ts", r#"export const b = "b";"#);
let mut client = context.new_lsp_command().capture_stderr().build();
client.initialize_default();
// now create a symlink in the current directory to a subdir/deno.json
// and ensure the watched files notification still works
temp_dir.create_dir_all("subdir");
temp_dir.write("subdir/deno.json", r#"{ }"#);
temp_dir.symlink_file(
temp_dir.path().join("subdir").join("deno.json"),
temp_dir.path().join("deno.json"),
);
client.did_change_watched_files(json!({
"changes": [{
// the client will give a watched file changed event for the symlink's target
"uri": temp_dir.path().join("subdir/deno.json").canonicalize().url_file(),
"type": 2
}]
}));
// this will discover the deno.json in the root
let search_line = format!(
" Resolved Deno configuration file: \"{}\"",
temp_dir.url().join("deno.json").unwrap().as_str()
);
client.wait_until_stderr_line(|line| line.contains(&search_line));
// now open a file which will cause a diagnostic because the import map is empty
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("a.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": "import { b } from \"/~/b.ts\";\n\nconsole.log(b);\n"
}
}));
assert_eq!(diagnostics.all().len(), 1);
// update the import map to have the imports now
temp_dir.write("subdir/deno.json", r#"{ "imports": { "/~/": "./lib/" } }"#);
client.did_change_watched_files(json!({
"changes": [{
// now still say that the target path has changed
"uri": temp_dir.path().join("subdir/deno.json").canonicalize().url_file(),
"type": 2
}]
}));
assert_eq!(client.read_diagnostics().all().len(), 0);
client.shutdown();
}
#[test]
fn lsp_deno_json_imports_comments_cache() {
let context = TestContextBuilder::new()
.use_http_server()
.use_temp_cwd()
.build();
let temp_dir = context.temp_dir();
temp_dir.write(
"deno.jsonc",
r#"{
// comment
"imports": {
"print_hello": "http://localhost:4545/import_maps/print_hello.ts",
},
}"#,
);
temp_dir.write(
"file.ts",
r#"
import { printHello } from "print_hello";
printHello();
"#,
);
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.write_request(
"workspace/executeCommand",
json!({
"command": "deno.cache",
"arguments": [[], temp_dir.url().join("file.ts").unwrap()],
}),
);
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": temp_dir.read_to_string("file.ts"),
}
}));
assert_eq!(diagnostics.all(), vec![]);
client.shutdown();
}
#[test]
fn lsp_import_map_node_specifiers() {
let context = TestContextBuilder::for_npm().use_temp_cwd().build();
let temp_dir = context.temp_dir();
temp_dir.write("deno.json", r#"{ "imports": { "fs": "node:fs" } }"#);
// cache @types/node
context
.new_command()
.args("cache npm:@types/node")
.run()
.skip_output_check()
.assert_exit_code(0);
let mut client = context.new_lsp_command().build();
client.initialize(|builder| {
builder.set_config("./deno.json");
});
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("a.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": "import fs from \"fs\";\nconsole.log(fs);"
}
}));
assert_eq!(diagnostics.all(), vec![]);
client.shutdown();
}
#[test]
fn lsp_format_vendor_path() {
let context = TestContextBuilder::new()
.use_http_server()
.use_temp_cwd()
.build();
// put this dependency in the global cache
context
.new_command()
.args("cache --allow-import http://localhost:4545/run/002_hello.ts")
.run()
.skip_output_check();
let temp_dir = context.temp_dir();
temp_dir.write("deno.json", json!({ "vendor": true }).to_string());
let mut client = context.new_lsp_command().build();
client.initialize_default();
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": r#"import "http://localhost:4545/run/002_hello.ts";"#,
},
}));
// copying from the global cache to the local cache requires explicitly
// running the cache command so that the checksums can be verified
assert_eq!(
diagnostics
.all()
.iter()
.map(|d| d.message.as_str())
.collect::>(),
vec![
"Uncached or missing remote URL: http://localhost:4545/run/002_hello.ts"
]
);
client.write_request(
"workspace/executeCommand",
json!({
"command": "deno.cache",
"arguments": [[], temp_dir.url().join("file.ts").unwrap()],
}),
);
assert!(temp_dir
.path()
.join("vendor/http_localhost_4545/run/002_hello.ts")
.exists());
client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("vendor/http_localhost_4545/run/002_hello.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": r#"console.log("Hello World");"#,
},
}));
let res = client.write_request(
"textDocument/formatting",
json!({
"textDocument": {
"uri": temp_dir.url().join("vendor/http_localhost_4545/run/002_hello.ts").unwrap(),
},
"options": {
"tabSize": 2,
"insertSpaces": true,
}
}),
);
assert_eq!(
res,
json!([{
"range": {
"start": {
"line": 0,
"character": 27,
},
"end": {
"line": 0,
"character": 27,
},
},
"newText": "\n",
}]),
);
client.shutdown();
}
// Regression test for https://github.com/denoland/deno/issues/19802.
// Disable the `workspace/configuration` capability. Ensure the LSP falls back
// to using `enablePaths` from the `InitializationOptions`.
#[test]
fn lsp_workspace_enable_paths_no_workspace_configuration() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
temp_dir.write("main_disabled.ts", "Date.now()");
temp_dir.write("main_enabled.ts", "Date.now()");
let mut client = context.new_lsp_command().build();
client.initialize(|builder| {
builder.with_capabilities(|capabilities| {
capabilities.workspace.as_mut().unwrap().configuration = Some(false);
});
builder.set_workspace_folders(vec![lsp::WorkspaceFolder {
uri: temp_dir.uri(),
name: "project".to_string(),
}]);
builder.set_root_uri(temp_dir.url());
builder.set_enable_paths(vec!["./main_enabled.ts".to_string()]);
});
client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("main_disabled.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": temp_dir.read_to_string("main_disabled.ts"),
}
}));
client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("main_enabled.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": temp_dir.read_to_string("main_enabled.ts"),
}
}));
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": temp_dir.url().join("main_disabled.ts").unwrap(),
},
"position": { "line": 0, "character": 5 }
}),
);
assert_eq!(res, json!(null));
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": temp_dir.url().join("main_enabled.ts").unwrap(),
},
"position": { "line": 0, "character": 5 }
}),
);
assert_eq!(
res,
json!({
"contents": [
{
"language": "typescript",
"value": "(method) DateConstructor.now(): number",
},
"Returns the number of milliseconds elapsed since midnight, January 1, 1970 Universal Coordinated Time (UTC)."
],
"range": {
"start": { "line": 0, "character": 5, },
"end": { "line": 0, "character": 8, }
}
})
);
client.shutdown();
}
#[test]
fn lsp_did_refresh_deno_configuration_tree_notification() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
temp_dir.create_dir_all("workspace/member1");
temp_dir.create_dir_all("workspace/member2");
temp_dir.create_dir_all("non_workspace1");
temp_dir.create_dir_all("non_workspace2");
temp_dir.write(
"workspace/deno.json",
json!({
"workspace": [
"member1",
"member2",
],
})
.to_string(),
);
temp_dir.write("workspace/member1/deno.json", json!({}).to_string());
temp_dir.write("workspace/member1/package.json", json!({}).to_string());
temp_dir.write("workspace/member2/package.json", json!({}).to_string());
temp_dir.write("non_workspace1/deno.json", json!({}).to_string());
let mut client = context.new_lsp_command().build();
client.initialize_default();
let res = client
.read_notification_with_method::(
"deno/didRefreshDenoConfigurationTree",
)
.unwrap();
assert_eq!(
res,
json!({
"data": [
{
"scopeUri": temp_dir.url().join("non_workspace1/").unwrap(),
"workspaceRootScopeUri": null,
"denoJson": {
"uri": temp_dir.url().join("non_workspace1/deno.json").unwrap(),
},
"packageJson": null,
},
{
"scopeUri": temp_dir.url().join("workspace/").unwrap(),
"workspaceRootScopeUri": null,
"denoJson": {
"uri": temp_dir.url().join("workspace/deno.json").unwrap(),
},
"packageJson": null,
},
{
"scopeUri": temp_dir.url().join("workspace/member1/").unwrap(),
"workspaceRootScopeUri": temp_dir.url().join("workspace/").unwrap(),
"denoJson": {
"uri": temp_dir.url().join("workspace/member1/deno.json").unwrap(),
},
"packageJson": {
"uri": temp_dir.url().join("workspace/member1/package.json").unwrap(),
},
},
{
"scopeUri": temp_dir.url().join("workspace/member2/").unwrap(),
"workspaceRootScopeUri": temp_dir.url().join("workspace/").unwrap(),
"denoJson": null,
"packageJson": {
"uri": temp_dir.url().join("workspace/member2/package.json").unwrap(),
},
},
],
}),
);
temp_dir.write("non_workspace2/deno.json", json!({}).to_string());
client.did_change_watched_files(json!({
"changes": [{
"uri": temp_dir.url().join("non_workspace2/deno.json").unwrap(),
"type": 1,
}],
}));
let res = client
.read_notification_with_method::(
"deno/didRefreshDenoConfigurationTree",
)
.unwrap();
assert_eq!(
res,
json!({
"data": [
{
"scopeUri": temp_dir.url().join("non_workspace1/").unwrap(),
"workspaceRootScopeUri": null,
"denoJson": {
"uri": temp_dir.url().join("non_workspace1/deno.json").unwrap(),
},
"packageJson": null,
},
{
"scopeUri": temp_dir.url().join("non_workspace2/").unwrap(),
"workspaceRootScopeUri": null,
"denoJson": {
"uri": temp_dir.url().join("non_workspace2/deno.json").unwrap(),
},
"packageJson": null,
},
{
"scopeUri": temp_dir.url().join("workspace/").unwrap(),
"workspaceRootScopeUri": null,
"denoJson": {
"uri": temp_dir.url().join("workspace/deno.json").unwrap(),
},
"packageJson": null,
},
{
"scopeUri": temp_dir.url().join("workspace/member1/").unwrap(),
"workspaceRootScopeUri": temp_dir.url().join("workspace/").unwrap(),
"denoJson": {
"uri": temp_dir.url().join("workspace/member1/deno.json").unwrap(),
},
"packageJson": {
"uri": temp_dir.url().join("workspace/member1/package.json").unwrap(),
},
},
{
"scopeUri": temp_dir.url().join("workspace/member2/").unwrap(),
"workspaceRootScopeUri": temp_dir.url().join("workspace/").unwrap(),
"denoJson": null,
"packageJson": {
"uri": temp_dir.url().join("workspace/member2/package.json").unwrap(),
},
},
],
}),
);
client.change_configuration(json!({
"deno": {
"disablePaths": ["non_workspace1"],
},
}));
let res = client
.read_notification_with_method::(
"deno/didRefreshDenoConfigurationTree",
)
.unwrap();
assert_eq!(
res,
json!({
"data": [
{
"scopeUri": temp_dir.url().join("non_workspace2/").unwrap(),
"workspaceRootScopeUri": null,
"denoJson": {
"uri": temp_dir.url().join("non_workspace2/deno.json").unwrap(),
},
"packageJson": null,
},
{
"scopeUri": temp_dir.url().join("workspace/").unwrap(),
"workspaceRootScopeUri": null,
"denoJson": {
"uri": temp_dir.url().join("workspace/deno.json").unwrap(),
},
"packageJson": null,
},
{
"scopeUri": temp_dir.url().join("workspace/member1/").unwrap(),
"workspaceRootScopeUri": temp_dir.url().join("workspace/").unwrap(),
"denoJson": {
"uri": temp_dir.url().join("workspace/member1/deno.json").unwrap(),
},
"packageJson": {
"uri": temp_dir.url().join("workspace/member1/package.json").unwrap(),
},
},
{
"scopeUri": temp_dir.url().join("workspace/member2/").unwrap(),
"workspaceRootScopeUri": temp_dir.url().join("workspace/").unwrap(),
"denoJson": null,
"packageJson": {
"uri": temp_dir.url().join("workspace/member2/package.json").unwrap(),
},
},
],
}),
);
client.shutdown();
}
#[test]
fn lsp_did_change_deno_configuration_notification() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
temp_dir.write("deno.json", json!({}).to_string());
temp_dir.write("package.json", json!({}).to_string());
let mut client = context.new_lsp_command().build();
client.initialize_default();
let res = client
.read_notification_with_method::("deno/didChangeDenoConfiguration");
assert_eq!(
res,
Some(json!({
"changes": [
{
"scopeUri": temp_dir.url(),
"fileUri": temp_dir.url().join("deno.json").unwrap(),
"type": "added",
"configurationType": "denoJson"
},
{
"scopeUri": temp_dir.url(),
"fileUri": temp_dir.url().join("package.json").unwrap(),
"type": "added",
"configurationType": "packageJson"
},
],
}))
);
temp_dir.write(
"deno.json",
json!({ "fmt": { "semiColons": false } }).to_string(),
);
client.did_change_watched_files(json!({
"changes": [{
"uri": temp_dir.url().join("deno.json").unwrap(),
"type": 2,
}],
}));
let res = client
.read_notification_with_method::("deno/didChangeDenoConfiguration");
assert_eq!(
res,
Some(json!({
"changes": [{
"scopeUri": temp_dir.url(),
"fileUri": temp_dir.url().join("deno.json").unwrap(),
"type": "changed",
"configurationType": "denoJson"
}],
}))
);
temp_dir.remove_file("deno.json");
client.did_change_watched_files(json!({
"changes": [{
"uri": temp_dir.url().join("deno.json").unwrap(),
"type": 3,
}],
}));
let res = client
.read_notification_with_method::("deno/didChangeDenoConfiguration");
assert_eq!(
res,
Some(json!({
"changes": [{
"scopeUri": temp_dir.url(),
"fileUri": temp_dir.url().join("deno.json").unwrap(),
"type": "removed",
"configurationType": "denoJson"
}],
}))
);
temp_dir.write("package.json", json!({ "type": "module" }).to_string());
client.did_change_watched_files(json!({
"changes": [{
"uri": temp_dir.url().join("package.json").unwrap(),
"type": 2,
}],
}));
let res = client
.read_notification_with_method::("deno/didChangeDenoConfiguration");
assert_eq!(
res,
Some(json!({
"changes": [{
"scopeUri": temp_dir.url(),
"fileUri": temp_dir.url().join("package.json").unwrap(),
"type": "changed",
"configurationType": "packageJson"
}],
}))
);
temp_dir.remove_file("package.json");
client.did_change_watched_files(json!({
"changes": [{
"uri": temp_dir.url().join("package.json").unwrap(),
"type": 3,
}],
}));
let res = client
.read_notification_with_method::("deno/didChangeDenoConfiguration");
assert_eq!(
res,
Some(json!({
"changes": [{
"scopeUri": temp_dir.url(),
"fileUri": temp_dir.url().join("package.json").unwrap(),
"type": "removed",
"configurationType": "packageJson"
}],
}))
);
client.shutdown();
}
#[test]
fn lsp_deno_task() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
temp_dir.write(
"deno.jsonc",
json!({
"tasks": {
"build": "deno test",
"serve": {
"description": "Start the dev server",
"command": "deno run -RN server.ts",
},
},
})
.to_string(),
);
let mut client = context.new_lsp_command().build();
client.initialize_default();
let res = client.write_request("deno/taskDefinitions", json!(null));
assert_eq!(
res,
json!([
{
"name": "build",
"command": "deno test",
"sourceUri": temp_dir.url().join("deno.jsonc").unwrap(),
},
{
"name": "serve",
"command": "deno run -RN server.ts",
"sourceUri": temp_dir.url().join("deno.jsonc").unwrap(),
}
])
);
client.shutdown();
}
#[test]
fn lsp_reload_import_registries_command() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
let res = client.write_request(
"workspace/executeCommand",
json!({ "command": "deno.reloadImportRegistries" }),
);
assert_eq!(res, json!(true));
client.shutdown();
}
#[test]
fn lsp_import_attributes() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize(|builder| {
builder.set_import_map("data:application/json;utf8,{\"imports\": { \"example\": \"https://deno.land/x/example/mod.ts\" }}");
});
client.change_configuration(json!({
"deno": {
"enable": true,
"codeLens": {
"test": true,
},
},
}));
client.did_open(json!({
"textDocument": {
"uri": "file:///a/test.json",
"languageId": "json",
"version": 1,
"text": "{\"a\":1}",
},
}));
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": "file:///a/a.ts",
"languageId": "typescript",
"version": 1,
"text": "import a from \"./test.json\";\n\nconsole.log(a);\n"
}
}));
assert_eq!(
json!(
diagnostics
.messages_with_file_and_source("file:///a/a.ts", "deno")
.diagnostics
),
json!([
{
"range": {
"start": { "line": 0, "character": 14 },
"end": { "line": 0, "character": 27 }
},
"severity": 1,
"code": "no-attribute-type",
"source": "deno",
"message": "The module is a JSON module and not being imported with an import attribute. Consider adding `with { type: \"json\" }` to the import statement."
}
])
);
let res = client
.write_request(
"textDocument/codeAction",
json!({
"textDocument": {
"uri": "file:///a/a.ts"
},
"range": {
"start": { "line": 0, "character": 14 },
"end": { "line": 0, "character": 27 }
},
"context": {
"diagnostics": [{
"range": {
"start": { "line": 0, "character": 14 },
"end": { "line": 0, "character": 27 }
},
"severity": 1,
"code": "no-attribute-type",
"source": "deno",
"message": "The module is a JSON module and not being imported with an import attribute. Consider adding `with { type: \"json\" }` to the import statement."
}],
"only": ["quickfix"]
}
}),
);
assert_eq!(
res,
json!([{
"title": "Insert import attribute.",
"kind": "quickfix",
"diagnostics": [
{
"range": {
"start": { "line": 0, "character": 14 },
"end": { "line": 0, "character": 27 }
},
"severity": 1,
"code": "no-attribute-type",
"source": "deno",
"message": "The module is a JSON module and not being imported with an import attribute. Consider adding `with { type: \"json\" }` to the import statement."
}
],
"edit": {
"changes": {
"file:///a/a.ts": [
{
"range": {
"start": { "line": 0, "character": 27 },
"end": { "line": 0, "character": 27 }
},
"newText": " with { type: \"json\" }"
}
]
}
}
}])
);
client.shutdown();
}
#[test]
fn lsp_import_map_import_completions() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
temp_dir.write(
"deno.json",
json!({
"imports": {
"/~/": "./lib/",
"/#/": "./src/",
"fs": "https://example.com/fs/index.js",
"std/": "https://example.com/std@0.123.0/",
"lib/": "./lib/",
},
"scopes": {
"file:///": {
"file": "./file.ts",
},
},
})
.to_string(),
);
temp_dir.create_dir_all("lib");
temp_dir.write("lib/b.ts", r#"export const b = "b";"#);
let mut client = context.new_lsp_command().build();
client.initialize_default();
let uri = temp_dir.url().join("a.ts").unwrap();
client.did_open(json!({
"textDocument": {
"uri": uri,
"languageId": "typescript",
"version": 1,
"text": r#"
import * as b from "";
import * as b from "/~/";
import * as b from "lib/";
"#,
},
}));
let res = client.get_completion(
&uri,
(1, 28),
json!({ "triggerKind": 2, "triggerCharacter": "\"" }),
);
assert_eq!(
json!(res),
json!({
"isIncomplete": false,
"items": [
{
"label": ".",
"kind": 19,
"detail": "(local)",
"sortText": "1",
"insertText": ".",
"commitCharacters": ["\"", "'"],
}, {
"label": "..",
"kind": 19,
"detail": "(local)",
"sortText": "1",
"insertText": "..",
"commitCharacters": ["\"", "'"],
}, {
"label": "file",
"kind": 17,
"detail": "(import map)",
"sortText": "file",
"insertText": "file",
"commitCharacters": ["\"", "'"],
}, {
"label": "std",
"kind": 19,
"detail": "(import map)",
"sortText": "std",
"insertText": "std",
"commitCharacters": ["\"", "'"],
}, {
"label": "lib",
"kind": 19,
"detail": "(import map)",
"sortText": "lib",
"insertText": "lib",
"commitCharacters": ["\"", "'"],
}, {
"label": "fs",
"kind": 17,
"detail": "(import map)",
"sortText": "fs",
"insertText": "fs",
"commitCharacters": ["\"", "'"],
}, {
"label": "/~",
"kind": 19,
"detail": "(import map)",
"sortText": "/~",
"insertText": "/~",
"commitCharacters": ["\"", "'"],
}, {
"label": "/#",
"kind": 19,
"detail": "(import map)",
"sortText": "/#",
"insertText": "/#",
"commitCharacters": ["\"", "'"],
},
]
})
);
let res = client.get_completion(
&uri,
(2, 31),
json!({ "triggerKind": 2, "triggerCharacter": "/" }),
);
assert_eq!(
json!(res),
json!({
"isIncomplete": false,
"items": [
{
"label": "b.ts",
"kind": 17,
"detail": "(local)",
"sortText": "1",
"filterText": "/~/b.ts",
"textEdit": {
"range": {
"start": { "line": 2, "character": 28 },
"end": { "line": 2, "character": 31 },
},
"newText": "/~/b.ts",
},
"commitCharacters": ["\"", "'"],
},
],
}),
);
let res = client.get_completion(
&uri,
(3, 32),
json!({ "triggerKind": 2, "triggerCharacter": "/" }),
);
assert_eq!(
json!(res),
json!({
"isIncomplete": false,
"items": [
{
"label": "b.ts",
"kind": 17,
"detail": "(local)",
"sortText": "1",
"filterText": "lib/b.ts",
"textEdit": {
"range": {
"start": { "line": 3, "character": 28 },
"end": { "line": 3, "character": 32 },
},
"newText": "lib/b.ts",
},
"commitCharacters": ["\"", "'"],
},
],
}),
);
client.shutdown();
}
#[test]
fn lsp_hover() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "console.log(Deno.args);\n"
}
}));
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": { "line": 0, "character": 19 }
}),
);
assert_eq!(
res,
json!({
"contents": [
{
"language": "typescript",
"value": "const Deno.args: string[]"
},
"Returns the script arguments to the program.\n\nGive the following command line invocation of Deno:\n\n```sh\ndeno run --allow-read https://examples.deno.land/command-line-arguments.ts Sushi\n```\n\nThen `Deno.args` will contain:\n\n```ts\n[ \"Sushi\" ]\n```\n\nIf you are looking for a structured way to parse arguments, there is\n[`parseArgs()`](https://jsr.io/@std/cli/doc/parse-args/~/parseArgs) from\nthe Deno Standard Library.",
"\n\n*@category* - Runtime",
],
"range": {
"start": { "line": 0, "character": 17 },
"end": { "line": 0, "character": 21 }
}
})
);
client.shutdown();
}
#[test]
fn lsp_hover_asset() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
temp_dir.write("deno.json", json!({}).to_string());
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": "console.log(Date.now());\n"
}
}));
client.write_request(
"textDocument/definition",
json!({
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap()
},
"position": { "line": 0, "character": 14 }
}),
);
client.write_request(
"deno/virtualTextDocument",
json!({
"textDocument": {
"uri": "deno:/asset/lib.deno.shared_globals.d.ts"
}
}),
);
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": "deno:/asset/lib.es2015.symbol.wellknown.d.ts"
},
"position": { "line": 111, "character": 13 }
}),
);
assert_eq!(
res,
json!({
"contents": [
{
"language": "typescript",
"value": "interface Date",
},
"Enables basic storage and retrieval of dates and times.",
"\n\n*@category* - Temporal \n\n*@experimental*"
],
"range": {
"start": { "line": 111, "character": 10, },
"end": { "line": 111, "character": 14, }
}
})
);
client.shutdown();
}
#[test]
fn lsp_hover_disabled() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize(|builder| {
builder.set_deno_enable(false);
});
client.change_configuration(json!({ "deno": { "enable": false } }));
client.did_open(json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "console.log(Date.now());\n",
},
}));
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": { "line": 0, "character": 19 }
}),
);
assert_eq!(res, json!(null));
client.shutdown();
}
#[test]
fn lsp_inlay_hints() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.change_configuration(json!({
"deno": {
"enable": true,
},
"typescript": {
"inlayHints": {
"parameterNames": {
"enabled": "all",
},
"parameterTypes": {
"enabled": true,
},
"variableTypes": {
"enabled": true,
},
"propertyDeclarationTypes": {
"enabled": true,
},
"functionLikeReturnTypes": {
"enabled": true,
},
"enumMemberValues": {
"enabled": true,
},
},
},
}));
client.did_open(json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": r#"
function a(b: string) {
return b;
}
a("foo");
enum C {
A,
}
parseInt("123", 8);
const d = Date.now();
class E {
f = Date.now();
}
["a"].map((v) => v + v);
interface Bar {
someField: string;
}
function getBar(): Bar {
return { someField: "foo" };
}
// This shouldn't have a type hint because the variable name makes it
// redundant.
const bar = getBar();
const someValue = getBar();
"#,
},
}));
let res = client.write_request(
"textDocument/inlayHint",
json!({
"textDocument": {
"uri": "file:///a/file.ts",
},
"range": {
"start": { "line": 1, "character": 0 },
"end": { "line": 31, "character": 0, },
},
}),
);
assert_eq!(
res,
json!([
{
"position": { "line": 1, "character": 29 },
"label": [{ "value": ": " }, { "value": "string" }],
"kind": 1,
"paddingLeft": true,
}, {
"position": { "line": 5, "character": 10 },
"label": [
{
"value": "b",
"location": {
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 1, "character": 19 },
"end": { "line": 1, "character": 20 },
},
},
},
{ "value": ":" },
],
"kind": 2,
"paddingRight": true,
}, {
"position": { "line": 8, "character": 11 },
"label": "= 0",
"paddingLeft": true,
}, {
"position": { "line": 11, "character": 17 },
"label": [
{
"value": "string",
"location": {
"uri": "deno:/asset/lib.es5.d.ts",
"range": {
"start": { "line": 41, "character": 26 },
"end": { "line": 41, "character": 32 },
},
},
},
{ "value": ":" },
],
"kind": 2,
"paddingRight": true,
}, {
"position": { "line": 11, "character": 24 },
"label": [
{
"value": "radix",
"location": {
"uri": "deno:/asset/lib.es5.d.ts",
"range": {
"start": { "line": 41, "character": 42 },
"end": { "line": 41, "character": 47 },
},
},
},
{ "value": ":" },
],
"kind": 2,
"paddingRight": true,
}, {
"position": { "line": 13, "character": 15 },
"label": [{ "value": ": " }, { "value": "number" }],
"kind": 1,
"paddingLeft": true,
}, {
"position": { "line": 16, "character": 11 },
"label": [{ "value": ": " }, { "value": "number" }],
"kind": 1,
"paddingLeft": true,
}, {
"position": { "line": 19, "character": 18 },
"label": [
{
"value": "callbackfn",
"location": {
"uri": "deno:/asset/lib.es5.d.ts",
"range": {
"start": { "line": 1462, "character": 11 },
"end": { "line": 1462, "character": 21 },
},
},
},
{ "value": ":" },
],
"kind": 2,
"paddingRight": true,
}, {
"position": { "line": 19, "character": 20 },
"label": [{ "value": ": " }, { "value": "string" }],
"kind": 1,
"paddingLeft": true,
}, {
"position": { "line": 19, "character": 21 },
"label": [{ "value": ": " }, { "value": "string" }],
"kind": 1,
"paddingLeft": true,
}, {
"position": { "line": 30, "character": 23 },
"label": [
{ "value": ": " },
{
"value": "Bar",
"location": {
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 21, "character": 18 },
"end": { "line": 21, "character": 21 },
},
},
},
],
"kind": 1,
"paddingLeft": true,
},
]),
);
client.shutdown();
}
#[test]
fn lsp_inlay_hints_not_enabled() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": r#"function a(b: string) {
return b;
}
a("foo");
enum C {
A,
}
parseInt("123", 8);
const d = Date.now();
class E {
f = Date.now();
}
["a"].map((v) => v + v);
"#
}
}));
let res = client.write_request(
"textDocument/inlayHint",
json!({
"textDocument": {
"uri": "file:///a/file.ts",
},
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 19, "character": 0, }
}
}),
);
assert_eq!(res, json!(null));
client.shutdown();
}
#[test]
fn lsp_suggestion_actions_disabled() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.change_configuration(json!({
"deno": {
"enable": true,
"lint": false,
},
"typescript": {
"suggestionActions": {
"enabled": false,
},
},
}));
client.read_diagnostics();
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": r#"
// The settings should disable the suggestion for this to be async.
function asyncLikeFunction() {
return new Promise((r) => r(null)).then((v) => v);
}
console.log(asyncLikeFunction);
// Deprecated warnings should remain.
/** @deprecated */
function deprecatedFunction() {}
console.log(deprecatedFunction);
// Unused warnings should remain.
const unsusedVariable = 1;
"#,
},
}));
assert_eq!(
json!(diagnostics.all()),
json!([
{
"range": {
"start": { "line": 10, "character": 20 },
"end": { "line": 10, "character": 38 },
},
"severity": 4,
"code": 6385,
"source": "deno-ts",
"message": "'deprecatedFunction' is deprecated.",
"relatedInformation": [
{
"location": {
"uri": temp_dir.url().join("file.ts").unwrap(),
"range": {
"start": { "line": 8, "character": 12 },
"end": { "line": 8, "character": 24 },
},
},
"message": "The declaration was marked as deprecated here.",
},
],
"tags": [2],
},
{
"range": {
"start": { "line": 13, "character": 14 },
"end": { "line": 13, "character": 29 },
},
"severity": 4,
"code": 6133,
"source": "deno-ts",
"message": "'unsusedVariable' is declared but its value is never read.",
"tags": [1],
},
]),
);
client.shutdown();
}
#[test]
fn lsp_workspace_disable_enable_paths() {
fn run_test(use_trailing_slash: bool) {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
temp_dir.create_dir_all("worker");
temp_dir.write("worker/shared.ts", "export const a = 1");
temp_dir.write("worker/other.ts", "import { a } from './shared.ts';\na;");
temp_dir.write("worker/node.ts", "Buffer.alloc(1);");
let root_specifier = temp_dir.url();
let mut client = context.new_lsp_command().build();
client.initialize_with_config(
|builder| {
builder
.set_disable_paths(vec!["./worker/node.ts".to_string()])
.set_enable_paths(vec!["./worker".to_string()])
.set_root_uri(root_specifier.clone())
.set_workspace_folders(vec![lsp::WorkspaceFolder {
uri: if use_trailing_slash {
lsp::Uri::from_str(root_specifier.as_str()).unwrap()
} else {
lsp::Uri::from_str(
root_specifier.as_str().strip_suffix('/').unwrap(),
)
.unwrap()
},
name: "project".to_string(),
}]);
},
json!({ "deno": {
"disablePaths": ["./worker/node.ts"],
"enablePaths": ["./worker"],
} }),
);
client.did_open(json!({
"textDocument": {
"uri": root_specifier.join("./file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": "console.log(Date.now());\n"
}
}));
client.did_open(json!({
"textDocument": {
"uri": root_specifier.join("./other/file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": "console.log(Date.now());\n"
}
}));
client.did_open(json!({
"textDocument": {
"uri": root_specifier.join("./worker/file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": concat!(
"console.log(Date.now());\n",
"import { a } from './shared.ts';\n",
"a;\n",
),
}
}));
client.did_open(json!({
"textDocument": {
"uri": root_specifier.join("./worker/subdir/file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": "console.log(Date.now());\n"
}
}));
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": root_specifier.join("./file.ts").unwrap(),
},
"position": { "line": 0, "character": 19 }
}),
);
assert_eq!(res, json!(null));
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": root_specifier.join("./other/file.ts").unwrap(),
},
"position": { "line": 0, "character": 19 }
}),
);
assert_eq!(res, json!(null));
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": root_specifier.join("./worker/node.ts").unwrap(),
},
"position": { "line": 0, "character": 0 }
}),
);
assert_eq!(res, json!(null));
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": root_specifier.join("./worker/file.ts").unwrap(),
},
"position": { "line": 0, "character": 19 }
}),
);
assert_eq!(
res,
json!({
"contents": [
{
"language": "typescript",
"value": "(method) DateConstructor.now(): number",
},
"Returns the number of milliseconds elapsed since midnight, January 1, 1970 Universal Coordinated Time (UTC)."
],
"range": {
"start": { "line": 0, "character": 17, },
"end": { "line": 0, "character": 20, }
}
})
);
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": root_specifier.join("./worker/subdir/file.ts").unwrap(),
},
"position": { "line": 0, "character": 19 }
}),
);
assert_eq!(
res,
json!({
"contents": [
{
"language": "typescript",
"value": "(method) DateConstructor.now(): number",
},
"Returns the number of milliseconds elapsed since midnight, January 1, 1970 Universal Coordinated Time (UTC)."
],
"range": {
"start": { "line": 0, "character": 17, },
"end": { "line": 0, "character": 20, }
}
})
);
// check that the file system documents were auto-discovered
// via the enabled paths
let res = client.write_request(
"textDocument/references",
json!({
"textDocument": {
"uri": root_specifier.join("./worker/file.ts").unwrap(),
},
"position": { "line": 2, "character": 0 },
"context": {
"includeDeclaration": true
}
}),
);
assert_eq!(
res,
json!([{
"uri": root_specifier.join("./worker/file.ts").unwrap(),
"range": {
"start": { "line": 1, "character": 9 },
"end": { "line": 1, "character": 10 }
}
}, {
"uri": root_specifier.join("./worker/file.ts").unwrap(),
"range": {
"start": { "line": 2, "character": 0 },
"end": { "line": 2, "character": 1 }
}
}, {
"uri": root_specifier.join("./worker/shared.ts").unwrap(),
"range": {
"start": { "line": 0, "character": 13 },
"end": { "line": 0, "character": 14 }
}
}, {
"uri": root_specifier.join("./worker/other.ts").unwrap(),
"range": {
"start": { "line": 0, "character": 9 },
"end": { "line": 0, "character": 10 }
}
}, {
"uri": root_specifier.join("./worker/other.ts").unwrap(),
"range": {
"start": { "line": 1, "character": 0 },
"end": { "line": 1, "character": 1 }
}
}])
);
client.shutdown();
}
run_test(true);
run_test(false);
}
#[test]
fn lsp_exclude_config() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
temp_dir.create_dir_all("other");
temp_dir.write(
"other/shared.ts",
// this should not be found in the "find references" since this file is excluded
"import { a } from '../worker/shared.ts'; console.log(a);",
);
temp_dir.create_dir_all("worker");
temp_dir.write("worker/shared.ts", "export const a = 1");
temp_dir.write(
"deno.json",
r#"{
"exclude": ["other"],
}"#,
);
let root_specifier = temp_dir.url();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(json!({
"textDocument": {
"uri": root_specifier.join("./other/file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": "console.log(Date.now());\n"
}
}));
client.did_open(json!({
"textDocument": {
"uri": root_specifier.join("./worker/file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": concat!(
"console.log(Date.now());\n",
"import { a } from './shared.ts';\n",
"a;\n",
),
}
}));
client.did_open(json!({
"textDocument": {
"uri": root_specifier.join("./worker/subdir/file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": "console.log(Date.now());\n"
}
}));
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": root_specifier.join("./other/file.ts").unwrap(),
},
"position": { "line": 0, "character": 19 }
}),
);
assert_eq!(res, json!(null));
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": root_specifier.join("./worker/file.ts").unwrap(),
},
"position": { "line": 0, "character": 19 }
}),
);
assert_eq!(
res,
json!({
"contents": [
{
"language": "typescript",
"value": "(method) DateConstructor.now(): number",
},
"Returns the number of milliseconds elapsed since midnight, January 1, 1970 Universal Coordinated Time (UTC)."
],
"range": {
"start": { "line": 0, "character": 17, },
"end": { "line": 0, "character": 20, }
}
})
);
// check that the file system documents were auto-discovered
let res = client.write_request(
"textDocument/references",
json!({
"textDocument": {
"uri": root_specifier.join("./worker/file.ts").unwrap(),
},
"position": { "line": 2, "character": 0 },
"context": {
"includeDeclaration": true
}
}),
);
assert_eq!(
res,
json!([{
"uri": root_specifier.join("./worker/file.ts").unwrap(),
"range": {
"start": { "line": 1, "character": 9 },
"end": { "line": 1, "character": 10 }
}
}, {
"uri": root_specifier.join("./worker/file.ts").unwrap(),
"range": {
"start": { "line": 2, "character": 0 },
"end": { "line": 2, "character": 1 }
}
}, {
"uri": root_specifier.join("./worker/shared.ts").unwrap(),
"range": {
"start": { "line": 0, "character": 13 },
"end": { "line": 0, "character": 14 }
}
}])
);
client.shutdown();
}
#[test]
fn lsp_hover_unstable_always_enabled() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
// IMPORTANT: If you change this API due to stabilization, also change it
// in the enabled test below.
"text": "type _ = Deno.DatagramConn;\n"
}
}));
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": { "line": 0, "character": 14 }
}),
);
assert_eq!(
res,
json!({
"contents":[
{
"language":"typescript",
"value":"interface Deno.DatagramConn"
},
"**UNSTABLE**: New API, yet to be vetted.\n\nA generic transport listener for message-oriented protocols.",
"\n\n*@category* - Network \n\n*@experimental*",
],
"range":{
"start":{ "line":0, "character":14 },
"end":{ "line":0, "character":26 }
}
})
);
client.shutdown();
}
#[test]
fn lsp_hover_unstable_enabled() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize(|builder| {
// NOTE(bartlomieju): this is effectively not used anymore.
builder.set_unstable(true);
});
client.did_open(json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "type _ = Deno.DatagramConn;\n"
}
}));
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": { "line": 0, "character": 14 }
}),
);
assert_eq!(
res,
json!({
"contents":[
{
"language":"typescript",
"value":"interface Deno.DatagramConn"
},
"**UNSTABLE**: New API, yet to be vetted.\n\nA generic transport listener for message-oriented protocols.",
"\n\n*@category* - Network \n\n*@experimental*",
],
"range":{
"start":{ "line":0, "character":14 },
"end":{ "line":0, "character":26 }
}
})
);
client.shutdown();
}
#[test]
fn lsp_hover_change_mbc() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "const a = `编写软件很难`;\nconst b = `👍🦕😃`;\nconsole.log(a, b);\n"
}
}),
);
client.write_notification(
"textDocument/didChange",
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"version": 2
},
"contentChanges": [
{
"range": {
"start": { "line": 1, "character": 11 },
"end": {
"line": 1,
// the LSP uses utf16 encoded characters indexes, so
// after the deno emoji is character index 15
"character": 15
}
},
"text": ""
}
]
}),
);
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": { "line": 2, "character": 15 }
}),
);
assert_eq!(
res,
json!({
"contents": [
{
"language": "typescript",
"value": "const b: \"😃\"",
},
"",
],
"range": {
"start": { "line": 2, "character": 15, },
"end": { "line": 2, "character": 16, },
}
})
);
client.shutdown();
}
#[test]
fn lsp_hover_closed_document() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
temp_dir.write("a.ts", r#"export const a = "a";"#);
temp_dir.write("b.ts", r#"export * from "./a.ts";"#);
temp_dir.write("c.ts", "import { a } from \"./b.ts\";\nconsole.log(a);\n");
let b_specifier = temp_dir.url().join("b.ts").unwrap();
let c_specifier = temp_dir.url().join("c.ts").unwrap();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(json!({
"textDocument": {
"uri": b_specifier,
"languageId": "typescript",
"version": 1,
"text": r#"export * from "./a.ts";"#
}
}));
client.did_open(json!({
"textDocument": {
"uri": c_specifier,
"languageId": "typescript",
"version": 1,
"text": "import { a } from \"./b.ts\";\nconsole.log(a);\n",
}
}));
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": c_specifier,
},
"position": { "line": 0, "character": 10 }
}),
);
assert_eq!(
res,
json!({
"contents": [
{
"language": "typescript",
"value": "(alias) const a: \"a\"\nimport a"
},
""
],
"range": {
"start": { "line": 0, "character": 9 },
"end": { "line": 0, "character": 10 }
}
})
);
client.write_notification(
"textDocument/didClose",
json!({
"textDocument": {
"uri": b_specifier,
}
}),
);
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": c_specifier,
},
"position": { "line": 0, "character": 10 }
}),
);
assert_eq!(
res,
json!({
"contents": [
{
"language": "typescript",
"value": "(alias) const a: \"a\"\nimport a"
},
""
],
"range": {
"start": { "line": 0, "character": 9 },
"end": { "line": 0, "character": 10 }
}
})
);
client.shutdown();
}
#[test]
fn lsp_hover_dependency() {
let context = TestContextBuilder::new()
.use_http_server()
.use_temp_cwd()
.build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(json!({
"textDocument": {
"uri": "file:///a/file_01.ts",
"languageId": "typescript",
"version": 1,
"text": "export const a = \"a\";\n",
}
}));
client.did_open(
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "import * as a from \"http://127.0.0.1:4545/xTypeScriptTypes.js\";\n// @ts-types=\"http://127.0.0.1:4545/type_definitions/foo.d.ts\"\nimport * as b from \"http://127.0.0.1:4545/type_definitions/foo.js\";\nimport * as c from \"http://127.0.0.1:4545/subdir/type_reference.js\";\nimport * as d from \"http://127.0.0.1:4545/subdir/mod1.ts\";\nimport * as e from \"data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=\";\nimport * as f from \"./file_01.ts\";\nimport * as g from \"http://localhost:4545/x/a/mod.ts\";\nimport * as h from \"./mod🦕.ts\";\n\nconsole.log(a, b, c, d, e, f, g, h);\n"
}
}),
);
client.write_request(
"workspace/executeCommand",
json!({
"command": "deno.cache",
"arguments": [[], "file:///a/file.ts"],
}),
);
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": "file:///a/file.ts",
},
"position": { "line": 0, "character": 28 }
}),
);
assert_eq!(
res,
json!({
"contents": {
"kind": "markdown",
"value": "**Resolved Dependency**\n\n**Code**: http://127.0.0.1:4545/xTypeScriptTypes.js\n\n**Types**: http://127.0.0.1:4545/xTypeScriptTypes.d.ts\n"
},
"range": {
"start": { "line": 0, "character": 19 },
"end":{ "line": 0, "character": 62 }
}
})
);
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": "file:///a/file.ts",
},
"position": { "line": 3, "character": 28 }
}),
);
assert_eq!(
res,
json!({
"contents": {
"kind": "markdown",
"value": "**Resolved Dependency**\n\n**Code**: http://127.0.0.1:4545/subdir/type_reference.js\n\n**Types**: http://127.0.0.1:4545/subdir/type_reference.d.ts\n"
},
"range": {
"start": { "line": 3, "character": 19 },
"end":{ "line": 3, "character": 67 }
}
})
);
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": "file:///a/file.ts",
},
"position": { "line": 4, "character": 28 }
}),
);
assert_eq!(
res,
json!({
"contents": {
"kind": "markdown",
"value": "**Resolved Dependency**\n\n**Code**: http://127.0.0.1:4545/subdir/mod1.ts\n"
},
"range": {
"start": { "line": 4, "character": 19 },
"end":{ "line": 4, "character": 57 }
}
})
);
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": "file:///a/file.ts",
},
"position": { "line": 5, "character": 28 }
}),
);
assert_eq!(
res,
json!({
"contents": {
"kind": "markdown",
"value": "**Resolved Dependency**\n\n**Code**: _(a data url)_\n"
},
"range": {
"start": { "line": 5, "character": 19 },
"end":{ "line": 5, "character": 132 }
}
})
);
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": "file:///a/file.ts",
},
"position": { "line": 6, "character": 28 }
}),
);
assert_eq!(
res,
json!({
"contents": {
"kind": "markdown",
"value": "**Resolved Dependency**\n\n**Code**: file:///a/file_01.ts\n"
},
"range": {
"start": { "line": 6, "character": 19 },
"end":{ "line": 6, "character": 33 }
}
})
);
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": "file:///a/file.ts",
},
"position": { "line": 8, "character": 28 }
}),
);
assert_eq!(
res,
json!({
"contents": {
"kind": "markdown",
"value": "**Resolved Dependency**\n\n**Code**: file:///a/mod🦕.ts\n"
},
"range": {
"start": { "line": 8, "character": 19 },
"end":{ "line": 8, "character": 30 }
}
})
);
client.shutdown();
}
// This tests for a regression covered by denoland/deno#12753 where the lsp was
// unable to resolve dependencies when there was an invalid syntax in the module
#[test]
fn lsp_hover_deps_preserved_when_invalid_parse() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(json!({
"textDocument": {
"uri": "file:///a/file1.ts",
"languageId": "typescript",
"version": 1,
"text": "export type Foo = { bar(): string };\n"
}
}));
client.did_open(json!({
"textDocument": {
"uri": "file:///a/file2.ts",
"languageId": "typescript",
"version": 1,
"text": "import { Foo } from './file1.ts'; declare const f: Foo; f\n"
}
}));
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": "file:///a/file2.ts"
},
"position": { "line": 0, "character": 56 }
}),
);
assert_eq!(
res,
json!({
"contents": [
{
"language": "typescript",
"value": "const f: Foo",
},
""
],
"range": {
"start": { "line": 0, "character": 56, },
"end": { "line": 0, "character": 57, }
}
})
);
client.write_notification(
"textDocument/didChange",
json!({
"textDocument": {
"uri": "file:///a/file2.ts",
"version": 2
},
"contentChanges": [
{
"range": {
"start": { "line": 0, "character": 57 },
"end": { "line": 0, "character": 58 }
},
"text": "."
}
]
}),
);
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": "file:///a/file2.ts"
},
"position": { "line": 0, "character": 56 }
}),
);
assert_eq!(
res,
json!({
"contents": [
{
"language": "typescript",
"value": "const f: Foo",
},
""
],
"range": {
"start": { "line": 0, "character": 56, },
"end": { "line": 0, "character": 57, }
}
})
);
client.shutdown();
}
// Regression test for https://github.com/denoland/vscode_deno/issues/1068.
#[test]
fn lsp_rename_synbol_file_scheme_edits_only() {
let context = TestContextBuilder::new()
.use_http_server()
.use_temp_cwd()
.build();
let temp_dir = context.temp_dir();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": r#"
import { SEPARATOR } from "http://localhost:4545/subdir/exports.ts";
console.log(SEPARATOR);
"#,
},
}));
let res = client.write_request(
"textDocument/rename",
json!({
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap(),
},
"position": { "line": 1, "character": 17 },
"newName": "PATH_SEPARATOR",
}),
);
assert_eq!(
res,
json!({
"documentChanges": [
{
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap(),
"version": 1,
},
"edits": [
{
"range": {
"start": { "line": 1, "character": 17 },
"end": { "line": 1, "character": 26 },
},
"newText": "PATH_SEPARATOR",
},
{
"range": {
"start": { "line": 2, "character": 20 },
"end": { "line": 2, "character": 29 },
},
"newText": "PATH_SEPARATOR",
},
],
}
],
})
);
client.shutdown();
}
// Regression test for https://github.com/denoland/deno/issues/23121.
#[test]
fn lsp_document_preload_limit_zero_deno_json_detection() {
let context = TestContextBuilder::new()
.use_http_server()
.use_temp_cwd()
.build();
let temp_dir = context.temp_dir();
temp_dir.write("deno.json", json!({}).to_string());
let mut client = context.new_lsp_command().build();
client.initialize(|builder| {
builder.set_preload_limit(0);
});
let res = client
.read_notification_with_method::("deno/didChangeDenoConfiguration");
assert_eq!(
res,
Some(json!({
"changes": [{
"scopeUri": temp_dir.url(),
"fileUri": temp_dir.url().join("deno.json").unwrap(),
"type": "added",
"configurationType": "denoJson",
}],
}))
);
client.shutdown();
}
// Regression test for https://github.com/denoland/deno/issues/23141.
#[test]
fn lsp_import_map_setting_with_deno_json() {
let context = TestContextBuilder::new()
.use_http_server()
.use_temp_cwd()
.build();
let temp_dir = context.temp_dir();
temp_dir.write("deno.json", json!({}).to_string());
temp_dir.write(
"import_map.json",
json!({
"imports": {
"file2": "./file2.ts",
},
})
.to_string(),
);
temp_dir.write("file2.ts", "");
let mut client = context.new_lsp_command().build();
client.initialize(|builder| {
builder.set_import_map("import_map.json");
});
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": "import \"file2\";\n",
},
}));
assert_eq!(json!(diagnostics.all()), json!([]));
client.shutdown();
}
#[test]
fn lsp_hover_typescript_types() {
let context = TestContextBuilder::new()
.use_http_server()
.use_temp_cwd()
.build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "import * as a from \"http://127.0.0.1:4545/xTypeScriptTypes.js\";\n\nconsole.log(a.foo);\n",
}
}),
);
client.write_request(
"workspace/executeCommand",
json!({
"command": "deno.cache",
"arguments": [
["http://127.0.0.1:4545/xTypeScriptTypes.js"],
"file:///a/file.ts",
],
}),
);
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": { "line": 0, "character": 24 }
}),
);
assert_eq!(
res,
json!({
"contents": {
"kind": "markdown",
"value": "**Resolved Dependency**\n\n**Code**: http://127.0.0.1:4545/xTypeScriptTypes.js\n\n**Types**: http://127.0.0.1:4545/xTypeScriptTypes.d.ts\n"
},
"range": {
"start": { "line": 0, "character": 19 },
"end": { "line": 0, "character": 62 }
}
})
);
client.shutdown();
}
#[test]
fn lsp_hover_jsr() {
let context = TestContextBuilder::new()
.use_http_server()
.use_temp_cwd()
.build();
let temp_dir = context.temp_dir();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": "import \"jsr:@denotest/add@1.0.0\";\n",
}
}));
client.write_request(
"workspace/executeCommand",
json!({
"command": "deno.cache",
"arguments": [[], temp_dir.url().join("file.ts").unwrap()],
}),
);
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap(),
},
"position": { "line": 0, "character": 7 },
}),
);
assert_eq!(
res,
json!({
"contents": {
"kind": "markdown",
"value": "**Resolved Dependency**\n\n**Code**: jsr:@denotest/add@1.0.0 ()\n",
},
"range": {
"start": { "line": 0, "character": 7 },
"end": { "line": 0, "character": 32 },
},
}),
);
client.shutdown();
}
#[test]
fn lsp_hover_jsdoc_symbol_link() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(json!({
"textDocument": {
"uri": "file:///a/b.ts",
"languageId": "typescript",
"version": 1,
"text": "export function hello() {}\n"
}
}));
client.did_open(
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "import { hello } from \"./b.ts\";\n\nhello();\n\nconst b = \"b\";\n\n/** JSDoc {@link hello} and {@linkcode b} */\nfunction a() {}\n"
}
}),
);
let res = client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": { "line": 7, "character": 10 }
}),
);
assert_eq!(
res,
json!({
"contents": [
{
"language": "typescript",
"value": "function a(): void"
},
"JSDoc [hello](file:///a/b.ts#L1,1) and [`b`](file:///a/file.ts#L5,7)"
],
"range": {
"start": { "line": 7, "character": 9 },
"end": { "line": 7, "character": 10 }
}
})
);
client.shutdown();
}
#[test]
fn lsp_goto_type_definition() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "interface A {\n a: string;\n}\n\nexport class B implements A {\n a = \"a\";\n log() {\n console.log(this.a);\n }\n}\n\nconst b = new B();\nb;\n",
}
}),
);
let res = client.write_request(
"textDocument/typeDefinition",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": { "line": 12, "character": 1 }
}),
);
assert_eq!(
res,
json!([
{
"targetUri": "file:///a/file.ts",
"targetRange": {
"start": { "line": 4, "character": 0 },
"end": { "line": 9, "character": 1 }
},
"targetSelectionRange": {
"start": { "line": 4, "character": 13 },
"end": { "line": 4, "character": 14 }
}
}
])
);
client.shutdown();
}
#[test]
fn lsp_call_hierarchy() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "function foo() {\n return false;\n}\n\nclass Bar {\n baz() {\n return foo();\n }\n}\n\nfunction main() {\n const bar = new Bar();\n bar.baz();\n}\n\nmain();"
}
}),
);
let res = client.write_request(
"textDocument/prepareCallHierarchy",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": { "line": 5, "character": 3 }
}),
);
assert_eq!(
res,
json!([{
"name": "baz",
"kind": 6,
"detail": "Bar",
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 5, "character": 2 },
"end": { "line": 7, "character": 3 }
},
"selectionRange": {
"start": { "line": 5, "character": 2 },
"end": { "line": 5, "character": 5 }
}
}])
);
let res = client.write_request(
"callHierarchy/incomingCalls",
json!({
"item": {
"name": "baz",
"kind": 6,
"detail": "Bar",
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 5, "character": 2 },
"end": { "line": 7, "character": 3 }
},
"selectionRange": {
"start": { "line": 5, "character": 2 },
"end": { "line": 5, "character": 5 }
}
}
}),
);
assert_eq!(
res,
json!([{
"from": {
"name": "main",
"kind": 12,
"detail": "",
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 10, "character": 0 },
"end": { "line": 13, "character": 1 }
},
"selectionRange": {
"start": { "line": 10, "character": 9 },
"end": { "line": 10, "character": 13 }
}
},
"fromRanges": [
{
"start": { "line": 12, "character": 6 },
"end": { "line": 12, "character": 9 }
}
]
}])
);
let res = client.write_request(
"callHierarchy/outgoingCalls",
json!({
"item": {
"name": "baz",
"kind": 6,
"detail": "Bar",
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 5, "character": 2 },
"end": { "line": 7, "character": 3 }
},
"selectionRange": {
"start": { "line": 5, "character": 2 },
"end": { "line": 5, "character": 5 }
}
}
}),
);
assert_eq!(
res,
json!([{
"to": {
"name": "foo",
"kind": 12,
"detail": "",
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 2, "character": 1 }
},
"selectionRange": {
"start": { "line": 0, "character": 9 },
"end": { "line": 0, "character": 12 }
}
},
"fromRanges": [{
"start": { "line": 6, "character": 11 },
"end": { "line": 6, "character": 14 }
}]
}])
);
client.shutdown();
}
#[test]
fn lsp_large_doc_changes() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
let large_file_text =
fs::read_to_string(testdata_path().join("lsp").join("large_file.txt"))
.unwrap();
client.did_open(json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "javascript",
"version": 1,
"text": large_file_text,
}
}));
client.write_notification(
"textDocument/didChange",
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"version": 2
},
"contentChanges": [
{
"range": {
"start": { "line": 444, "character": 11 },
"end": { "line": 444, "character": 14 }
},
"text": "+++"
}
]
}),
);
client.write_notification(
"textDocument/didChange",
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"version": 2
},
"contentChanges": [
{
"range": {
"start": { "line": 445, "character": 4 },
"end": { "line": 445, "character": 4 }
},
"text": "// "
}
]
}),
);
client.write_notification(
"textDocument/didChange",
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"version": 2
},
"contentChanges": [
{
"range": {
"start": { "line": 477, "character": 4 },
"end": { "line": 477, "character": 9 }
},
"text": "error"
}
]
}),
);
client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": { "line": 421, "character": 30 }
}),
);
client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": { "line": 444, "character": 6 }
}),
);
client.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": { "line": 461, "character": 34 }
}),
);
client.shutdown();
assert!(client.duration().as_millis() <= 15000);
}
#[test]
fn lsp_document_symbol() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "interface IFoo {\n foo(): boolean;\n}\n\nclass Bar implements IFoo {\n constructor(public x: number) { }\n foo() { return true; }\n /** @deprecated */\n baz() { return false; }\n get value(): number { return 0; }\n set value(_newValue: number) { return; }\n static staticBar = new Bar(0);\n private static getStaticBar() { return Bar.staticBar; }\n}\n\nenum Values { value1, value2 }\n\nvar bar: IFoo = new Bar(3);"
}
}),
);
let res = client.write_request(
"textDocument/documentSymbol",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
}
}),
);
assert_eq!(
res,
json!([{
"name": "bar",
"kind": 13,
"range": {
"start": { "line": 17, "character": 4 },
"end": { "line": 17, "character": 26 }
},
"selectionRange": {
"start": { "line": 17, "character": 4 },
"end": { "line": 17, "character": 7 }
}
}, {
"name": "Bar",
"kind": 5,
"range": {
"start": { "line": 4, "character": 0 },
"end": { "line": 13, "character": 1 }
},
"selectionRange": {
"start": { "line": 4, "character": 6 },
"end": { "line": 4, "character": 9 }
},
"children": [{
"name": "constructor",
"kind": 9,
"range": {
"start": { "line": 5, "character": 2 },
"end": { "line": 5, "character": 35 }
},
"selectionRange": {
"start": { "line": 5, "character": 2 },
"end": { "line": 5, "character": 35 }
}
}, {
"name": "baz",
"kind": 6,
"tags": [1],
"range": {
"start": { "line": 8, "character": 2 },
"end": { "line": 8, "character": 25 }
},
"selectionRange": {
"start": { "line": 8, "character": 2 },
"end": { "line": 8, "character": 5 }
}
}, {
"name": "foo",
"kind": 6,
"range": {
"start": { "line": 6, "character": 2 },
"end": { "line": 6, "character": 24 }
},
"selectionRange": {
"start": { "line": 6, "character": 2 },
"end": { "line": 6, "character": 5 }
}
}, {
"name": "getStaticBar",
"kind": 6,
"range": {
"start": { "line": 12, "character": 2 },
"end": { "line": 12, "character": 57 }
},
"selectionRange": {
"start": { "line": 12, "character": 17 },
"end": { "line": 12, "character": 29 }
}
}, {
"name": "staticBar",
"kind": 8,
"range": {
"start": { "line": 11, "character": 2 },
"end": { "line": 11, "character": 32 }
},
"selectionRange": {
"start": { "line": 11, "character": 9 },
"end": { "line": 11, "character": 18 }
}
}, {
"name": "(get) value",
"kind": 8,
"range": {
"start": { "line": 9, "character": 2 },
"end": { "line": 9, "character": 35 }
},
"selectionRange": {
"start": { "line": 9, "character": 6 },
"end": { "line": 9, "character": 11 }
}
}, {
"name": "(set) value",
"kind": 8,
"range": {
"start": { "line": 10, "character": 2 },
"end": { "line": 10, "character": 42 }
},
"selectionRange": {
"start": { "line": 10, "character": 6 },
"end": { "line": 10, "character": 11 }
}
}, {
"name": "x",
"kind": 8,
"range": {
"start": { "line": 5, "character": 14 },
"end": { "line": 5, "character": 30 }
},
"selectionRange": {
"start": { "line": 5, "character": 21 },
"end": { "line": 5, "character": 22 }
}
}]
}, {
"name": "IFoo",
"kind": 11,
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 2, "character": 1 }
},
"selectionRange": {
"start": { "line": 0, "character": 10 },
"end": { "line": 0, "character": 14 }
},
"children": [{
"name": "foo",
"kind": 6,
"range": {
"start": { "line": 1, "character": 2 },
"end": { "line": 1, "character": 17 }
},
"selectionRange": {
"start": { "line": 1, "character": 2 },
"end": { "line": 1, "character": 5 }
}
}]
}, {
"name": "Values",
"kind": 10,
"range": {
"start": { "line": 15, "character": 0 },
"end": { "line": 15, "character": 30 }
},
"selectionRange": {
"start": { "line": 15, "character": 5 },
"end": { "line": 15, "character": 11 }
},
"children": [{
"name": "value1",
"kind": 22,
"range": {
"start": { "line": 15, "character": 14 },
"end": { "line": 15, "character": 20 }
},
"selectionRange": {
"start": { "line": 15, "character": 14 },
"end": { "line": 15, "character": 20 }
}
}, {
"name": "value2",
"kind": 22,
"range": {
"start": { "line": 15, "character": 22 },
"end": { "line": 15, "character": 28 }
},
"selectionRange": {
"start": { "line": 15, "character": 22 },
"end": { "line": 15, "character": 28 }
}
}]
}]
)
);
client.shutdown();
}
#[test]
fn lsp_folding_range() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "// #region 1\n/*\n * Some comment\n */\nclass Foo {\n bar(a, b) {\n if (a === b) {\n return true;\n }\n return false;\n }\n}\n// #endregion"
}
}),
);
let res = client.write_request(
"textDocument/foldingRange",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
}
}),
);
assert_eq!(
res,
json!([{
"startLine": 0,
"endLine": 12,
"kind": "region"
}, {
"startLine": 1,
"endLine": 3,
"kind": "comment"
}, {
"startLine": 4,
"endLine": 10
}, {
"startLine": 5,
"endLine": 9
}, {
"startLine": 6,
"endLine": 7
}])
);
client.shutdown();
}
#[test]
fn lsp_rename() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
// this should not rename in comments and strings
"text": "let variable = 'a'; // variable\nconsole.log(variable);\n\"variable\";\n"
}
}),
);
let res = client.write_request(
"textDocument/rename",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": { "line": 0, "character": 4 },
"newName": "variable_modified"
}),
);
assert_eq!(
res,
json!({
"documentChanges": [{
"textDocument": {
"uri": "file:///a/file.ts",
"version": 1
},
"edits": [{
"range": {
"start": { "line": 0, "character": 4 },
"end": { "line": 0, "character": 12 }
},
"newText": "variable_modified"
}, {
"range": {
"start": { "line": 1, "character": 12 },
"end": { "line": 1, "character": 20 }
},
"newText": "variable_modified"
}]
}]
})
);
client.shutdown();
}
#[test]
fn lsp_selection_range() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "class Foo {\n bar(a, b) {\n if (a === b) {\n return true;\n }\n return false;\n }\n}"
}
}),
);
let res = client.write_request(
"textDocument/selectionRange",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"positions": [{ "line": 2, "character": 8 }]
}),
);
assert_eq!(
res,
json!([{
"range": {
"start": { "line": 2, "character": 8 },
"end": { "line": 2, "character": 9 }
},
"parent": {
"range": {
"start": { "line": 2, "character": 8 },
"end": { "line": 2, "character": 15 }
},
"parent": {
"range": {
"start": { "line": 2, "character": 4 },
"end": { "line": 4, "character": 5 }
},
"parent": {
"range": {
"start": { "line": 1, "character": 13 },
"end": { "line": 6, "character": 2 }
},
"parent": {
"range": {
"start": { "line": 1, "character": 12 },
"end": { "line": 6, "character": 3 }
},
"parent": {
"range": {
"start": { "line": 1, "character": 2 },
"end": { "line": 6, "character": 3 }
},
"parent": {
"range": {
"start": { "line": 0, "character": 11 },
"end": { "line": 7, "character": 0 }
},
"parent": {
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 7, "character": 1 }
}
}
}
}
}
}
}
}
}])
);
client.shutdown();
}
#[test]
fn lsp_semantic_tokens() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "enum Values { value1, value2 }\n\nasync function baz(s: string): Promise {\n const r = s.slice(0);\n return r;\n}\n\ninterface IFoo {\n readonly x: number;\n foo(): boolean;\n}\n\nclass Bar implements IFoo {\n constructor(public readonly x: number) { }\n foo() { return true; }\n static staticBar = new Bar(0);\n private static getStaticBar() { return Bar.staticBar; }\n}\n"
}
}),
);
let res = client.write_request(
"textDocument/semanticTokens/full",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
}
}),
);
assert_eq!(
res,
json!({
"data": [
0, 5, 6, 1, 1, 0, 9, 6, 8, 9, 0, 8, 6, 8, 9, 2, 15, 3, 10, 5, 0, 4, 1,
6, 1, 0, 12, 7, 2, 16, 1, 8, 1, 7, 41, 0, 4, 1, 6, 0, 0, 2, 5, 11, 16,
1, 9, 1, 7, 40, 3, 10, 4, 2, 1, 1, 11, 1, 9, 9, 1, 2, 3, 11, 1, 3, 6, 3,
0, 1, 0, 15, 4, 2, 0, 1, 30, 1, 6, 9, 1, 2, 3, 11,1, 1, 9, 9, 9, 3, 0,
16, 3, 0, 0, 1, 17, 12, 11, 3, 0, 24, 3, 0, 0, 0, 4, 9, 9, 2
]
})
);
let res = client.write_request(
"textDocument/semanticTokens/range",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 6, "character": 0 }
}
}),
);
assert_eq!(
res,
json!({
"data": [
0, 5, 6, 1, 1, 0, 9, 6, 8, 9, 0, 8, 6, 8, 9, 2, 15, 3, 10, 5, 0, 4, 1,
6, 1, 0, 12, 7, 2, 16, 1, 8, 1, 7, 41, 0, 4, 1, 6, 0, 0, 2, 5, 11, 16,
1, 9, 1, 7, 40
]
})
);
client.shutdown();
}
#[test]
fn lsp_code_lens_references() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.change_configuration(json!({
"deno": {
"enable": true,
"codeLens": {
"references": true,
}
},
}));
client.did_open(json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": concat!(
"class A {\n",
" a = \"a\";\n",
"\n",
" b() {\n",
" console.log(this.a);\n",
" }\n",
"\n",
" c() {\n",
" this.a = \"c\";\n",
" }\n",
"}\n",
"\n",
"const a = new A();\n",
"a.b();\n",
"const b = 2;\n",
"const c = 3;\n",
"c; c;",
),
}
}));
let res = client.write_request(
"textDocument/codeLens",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
}
}),
);
assert_eq!(
res,
json!([{
"range": {
"start": { "line": 0, "character": 6 },
"end": { "line": 0, "character": 7 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}, {
"range": {
"start": { "line": 1, "character": 2 },
"end": { "line": 1, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}, {
"range": {
"start": { "line": 3, "character": 2 },
"end": { "line": 3, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}, {
"range": {
"start": { "line": 7, "character": 2 },
"end": { "line": 7, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}])
);
let res = client.write_request(
"codeLens/resolve",
json!({
"range": {
"start": { "line": 0, "character": 6 },
"end": { "line": 0, "character": 7 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}),
);
assert_eq!(
res,
json!({
"range": {
"start": { "line": 0, "character": 6 },
"end": { "line": 0, "character": 7 }
},
"command": {
"title": "1 reference",
"command": "deno.client.showReferences",
"arguments": [
"file:///a/file.ts",
{ "line": 0, "character": 6 },
[{
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 12, "character": 14 },
"end": { "line": 12, "character": 15 }
}
}]
]
}
})
);
// 0 references
let res = client.write_request(
"codeLens/resolve",
json!({
"range": {
"start": { "line": 14, "character": 6 },
"end": { "line": 14, "character": 7 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}),
);
assert_eq!(
res,
json!({
"range": {
"start": { "line": 14, "character": 6 },
"end": { "line": 14, "character": 7 }
},
"command": {
"title": "0 references",
"command": "",
}
})
);
// 2 references
let res = client.write_request(
"codeLens/resolve",
json!({
"range": {
"start": { "line": 15, "character": 6 },
"end": { "line": 15, "character": 7 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}),
);
assert_eq!(
res,
json!({
"range": {
"start": { "line": 15, "character": 6 },
"end": { "line": 15, "character": 7 }
},
"command": {
"title": "2 references",
"command": "deno.client.showReferences",
"arguments": [
"file:///a/file.ts",
{ "line": 15, "character": 6 },
[{
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 16, "character": 0 },
"end": { "line": 16, "character": 1 }
}
},{
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 16, "character": 3 },
"end": { "line": 16, "character": 4 }
}
}]
]
}
})
);
client.shutdown();
}
#[test]
fn lsp_code_lens_implementations() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.change_configuration(json!({
"deno": {
"enable": true,
"codeLens": {
"implementations": true,
"references": true,
}
},
}));
client.did_open(
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "interface A {\n b(): void;\n}\n\nclass B implements A {\n b() {\n console.log(\"b\");\n }\n}\n\ninterface C {\n c: string;\n}\n"
}
}),
);
let res = client.write_request(
"textDocument/codeLens",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
}
}),
);
assert_eq!(
res,
json!([ {
"range": {
"start": { "line": 0, "character": 10 },
"end": { "line": 0, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "implementations"
}
}, {
"range": {
"start": { "line": 0, "character": 10 },
"end": { "line": 0, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}, {
"range": {
"start": { "line": 1, "character": 2 },
"end": { "line": 1, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}, {
"range": {
"start": { "line": 4, "character": 6 },
"end": { "line": 4, "character": 7 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}, {
"range": {
"start": { "line": 5, "character": 2 },
"end": { "line": 5, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}, {
"range": {
"start": { "line": 10, "character": 10 },
"end": { "line": 10, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "implementations"
}
}, {
"range": {
"start": { "line": 10, "character": 10 },
"end": { "line": 10, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}, {
"range": {
"start": { "line": 11, "character": 2 },
"end": { "line": 11, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}])
);
let res = client.write_request(
"codeLens/resolve",
json!({
"range": {
"start": { "line": 0, "character": 10 },
"end": { "line": 0, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "implementations"
}
}),
);
assert_eq!(
res,
json!({
"range": {
"start": { "line": 0, "character": 10 },
"end": { "line": 0, "character": 11 }
},
"command": {
"title": "1 implementation",
"command": "deno.client.showReferences",
"arguments": [
"file:///a/file.ts",
{ "line": 0, "character": 10 },
[{
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 4, "character": 6 },
"end": { "line": 4, "character": 7 }
}
}]
]
}
})
);
let res = client.write_request(
"codeLens/resolve",
json!({
"range": {
"start": { "line": 10, "character": 10 },
"end": { "line": 10, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "implementations"
}
}),
);
assert_eq!(
res,
json!({
"range": {
"start": { "line": 10, "character": 10 },
"end": { "line": 10, "character": 11 }
},
"command": {
"title": "0 implementations",
"command": ""
}
})
);
client.shutdown();
}
#[test]
fn lsp_code_lens_test() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize(|builder| {
builder.disable_testing_api().set_code_lens(None);
});
client.did_open(
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "const { test } = Deno;\nconst { test: test2 } = Deno;\nconst test3 = Deno.test;\n\nDeno.test(\"test a\", () => {});\nDeno.test({\n name: \"test b\",\n fn() {},\n});\ntest({\n name: \"test c\",\n fn() {},\n});\ntest(\"test d\", () => {});\ntest2({\n name: \"test e\",\n fn() {},\n});\ntest2(\"test f\", () => {});\ntest3({\n name: \"test g\",\n fn() {},\n});\ntest3(\"test h\", () => {});\n"
}
}),
);
let res = client.write_request(
"textDocument/codeLens",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
}
}),
);
assert_eq!(
res,
json!([{
"range": {
"start": { "line": 4, "character": 5 },
"end": { "line": 4, "character": 9 }
},
"command": {
"title": "▶︎ Run Test",
"command": "deno.client.test",
"arguments": [
"file:///a/file.ts",
"test a",
{ "inspect": false }
]
}
}, {
"range": {
"start": { "line": 4, "character": 5 },
"end": { "line": 4, "character": 9 }
},
"command": {
"title": "Debug",
"command": "deno.client.test",
"arguments": [
"file:///a/file.ts",
"test a",
{ "inspect": true }
]
}
}, {
"range": {
"start": { "line": 5, "character": 5 },
"end": { "line": 5, "character": 9 }
},
"command": {
"title": "▶︎ Run Test",
"command": "deno.client.test",
"arguments": [
"file:///a/file.ts",
"test b",
{ "inspect": false }
]
}
}, {
"range": {
"start": { "line": 5, "character": 5 },
"end": { "line": 5, "character": 9 }
},
"command": {
"title": "Debug",
"command": "deno.client.test",
"arguments": [
"file:///a/file.ts",
"test b",
{ "inspect": true }
]
}
}, {
"range": {
"start": { "line": 9, "character": 0 },
"end": { "line": 9, "character": 4 }
},
"command": {
"title": "▶︎ Run Test",
"command": "deno.client.test",
"arguments": [
"file:///a/file.ts",
"test c",
{ "inspect": false }
]
}
}, {
"range": {
"start": { "line": 9, "character": 0 },
"end": { "line": 9, "character": 4 }
},
"command": {
"title": "Debug",
"command": "deno.client.test",
"arguments": [
"file:///a/file.ts",
"test c",
{ "inspect": true }
]
}
}, {
"range": {
"start": { "line": 13, "character": 0 },
"end": { "line": 13, "character": 4 }
},
"command": {
"title": "▶︎ Run Test",
"command": "deno.client.test",
"arguments": [
"file:///a/file.ts",
"test d",
{ "inspect": false }
]
}
}, {
"range": {
"start": { "line": 13, "character": 0 },
"end": { "line": 13, "character": 4 }
},
"command": {
"title": "Debug",
"command": "deno.client.test",
"arguments": [
"file:///a/file.ts",
"test d",
{ "inspect": true }
]
}
}, {
"range": {
"start": { "line": 14, "character": 0 },
"end": { "line": 14, "character": 5 }
},
"command": {
"title": "▶︎ Run Test",
"command": "deno.client.test",
"arguments": [
"file:///a/file.ts",
"test e",
{ "inspect": false }
]
}
}, {
"range": {
"start": { "line": 14, "character": 0 },
"end": { "line": 14, "character": 5 }
},
"command": {
"title": "Debug",
"command": "deno.client.test",
"arguments": [
"file:///a/file.ts",
"test e",
{ "inspect": true }
]
}
}, {
"range": {
"start": { "line": 18, "character": 0 },
"end": { "line": 18, "character": 5 }
},
"command": {
"title": "▶︎ Run Test",
"command": "deno.client.test",
"arguments": [
"file:///a/file.ts",
"test f",
{ "inspect": false }
]
}
}, {
"range": {
"start": { "line": 18, "character": 0 },
"end": { "line": 18, "character": 5 }
},
"command": {
"title": "Debug",
"command": "deno.client.test",
"arguments": [
"file:///a/file.ts",
"test f",
{ "inspect": true }
]
}
}, {
"range": {
"start": { "line": 19, "character": 0 },
"end": { "line": 19, "character": 5 }
},
"command": {
"title": "▶︎ Run Test",
"command": "deno.client.test",
"arguments": [
"file:///a/file.ts",
"test g",
{ "inspect": false }
]
}
}, {
"range": {
"start": { "line": 19, "character": 0 },
"end": { "line": 19, "character": 5 }
},
"command": {
"title": "Debug",
"command": "deno.client.test",
"arguments": [
"file:///a/file.ts",
"test g",
{ "inspect": true }
]
}
}, {
"range": {
"start": { "line": 23, "character": 0 },
"end": { "line": 23, "character": 5 }
},
"command": {
"title": "▶︎ Run Test",
"command": "deno.client.test",
"arguments": [
"file:///a/file.ts",
"test h",
{ "inspect": false }
]
}
}, {
"range": {
"start": { "line": 23, "character": 0 },
"end": { "line": 23, "character": 5 }
},
"command": {
"title": "Debug",
"command": "deno.client.test",
"arguments": [
"file:///a/file.ts",
"test h",
{ "inspect": true }
]
}
}])
);
client.shutdown();
}
#[test]
fn lsp_code_lens_test_disabled() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize(|builder| {
builder.disable_testing_api().set_code_lens(Some(json!({
"implementations": true,
"references": true,
"test": false
})));
});
client.change_configuration(json!({
"deno": {
"enable": true,
"codeLens": {
"test": false,
},
},
}));
client.did_open(json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "const { test } = Deno;\nconst { test: test2 } = Deno;\nconst test3 = Deno.test;\n\nDeno.test(\"test a\", () => {});\nDeno.test({\n name: \"test b\",\n fn() {},\n});\ntest({\n name: \"test c\",\n fn() {},\n});\ntest(\"test d\", () => {});\ntest2({\n name: \"test e\",\n fn() {},\n});\ntest2(\"test f\", () => {});\ntest3({\n name: \"test g\",\n fn() {},\n});\ntest3(\"test h\", () => {});\n"
},
}));
let res = client.write_request(
"textDocument/codeLens",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
}
}),
);
assert_eq!(res, json!(null));
client.shutdown();
}
#[test]
fn lsp_code_lens_non_doc_nav_tree() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.change_configuration(json!({
"deno": {
"enable": true,
"codeLens": {
"implementations": true,
"references": true,
}
},
}));
client.did_open(json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "console.log(Date.now());\n"
}
}));
client.write_request(
"textDocument/references",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": { "line": 0, "character": 3 },
"context": {
"includeDeclaration": true
}
}),
);
client.write_request(
"deno/virtualTextDocument",
json!({
"textDocument": {
"uri": "deno:/asset/lib.deno.shared_globals.d.ts"
}
}),
);
let res = client.write_request_with_res_as::>(
"textDocument/codeLens",
json!({
"textDocument": {
"uri": "deno:/asset/lib.deno.shared_globals.d.ts"
}
}),
);
assert!(res.len() > 50);
client.write_request_with_res_as::(
"codeLens/resolve",
json!({
"range": {
"start": { "line": 416, "character": 12 },
"end": { "line": 416, "character": 19 }
},
"data": {
"specifier": "asset:///lib.deno.shared_globals.d.ts",
"source": "references"
}
}),
);
client.shutdown();
}
#[test]
fn lsp_nav_tree_updates() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.change_configuration(json!({
"deno": {
"enable": true,
"codeLens": {
"implementations": true,
"references": true,
}
},
}));
client.did_open(
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "interface A {\n b(): void;\n}\n\nclass B implements A {\n b() {\n console.log(\"b\");\n }\n}\n\ninterface C {\n c: string;\n}\n"
}
}),
);
let res = client.write_request(
"textDocument/codeLens",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
}
}),
);
assert_eq!(
res,
json!([ {
"range": {
"start": { "line": 0, "character": 10 },
"end": { "line": 0, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "implementations"
}
}, {
"range": {
"start": { "line": 0, "character": 10 },
"end": { "line": 0, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}, {
"range": {
"start": { "line": 1, "character": 2 },
"end": { "line": 1, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}, {
"range": {
"start": { "line": 4, "character": 6 },
"end": { "line": 4, "character": 7 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}, {
"range": {
"start": { "line": 5, "character": 2 },
"end": { "line": 5, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}, {
"range": {
"start": { "line": 10, "character": 10 },
"end": { "line": 10, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "implementations"
}
}, {
"range": {
"start": { "line": 10, "character": 10 },
"end": { "line": 10, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}, {
"range": {
"start": { "line": 11, "character": 2 },
"end": { "line": 11, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}])
);
client.write_notification(
"textDocument/didChange",
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"version": 2
},
"contentChanges": [
{
"range": {
"start": { "line": 10, "character": 0 },
"end": { "line": 13, "character": 0 }
},
"text": ""
}
]
}),
);
let res = client.write_request(
"textDocument/codeLens",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
}
}),
);
assert_eq!(
res,
json!([{
"range": {
"start": { "line": 0, "character": 10 },
"end": { "line": 0, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "implementations"
}
}, {
"range": {
"start": { "line": 0, "character": 10 },
"end": { "line": 0, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}, {
"range": {
"start": { "line": 1, "character": 2 },
"end": { "line": 1, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}, {
"range": {
"start": { "line": 4, "character": 6 },
"end": { "line": 4, "character": 7 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}, {
"range": {
"start": { "line": 5, "character": 2 },
"end": { "line": 5, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references"
}
}])
);
client.shutdown();
}
#[test]
fn lsp_find_references() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(json!({
"textDocument": {
"uri": "file:///a/mod.ts",
"languageId": "typescript",
"version": 1,
"text": r"export const a = 1;\nconst b = 2;"
}
}));
client.did_open(json!({
"textDocument": {
"uri": "file:///a/mod.test.ts",
"languageId": "typescript",
"version": 1,
"text": r#"import { a } from './mod.ts'; console.log(a);"#
}
}));
// test without including the declaration
let res = client.write_request(
"textDocument/references",
json!({
"textDocument": {
"uri": "file:///a/mod.ts",
},
"position": { "line": 0, "character": 13 },
"context": {
"includeDeclaration": false
}
}),
);
assert_eq!(
res,
json!([{
"uri": "file:///a/mod.test.ts",
"range": {
"start": { "line": 0, "character": 9 },
"end": { "line": 0, "character": 10 }
}
}, {
"uri": "file:///a/mod.test.ts",
"range": {
"start": { "line": 0, "character": 42 },
"end": { "line": 0, "character": 43 }
}
}])
);
// test with including the declaration
let res = client.write_request(
"textDocument/references",
json!({
"textDocument": {
"uri": "file:///a/mod.ts",
},
"position": { "line": 0, "character": 13 },
"context": {
"includeDeclaration": true
}
}),
);
assert_eq!(
res,
json!([{
"uri": "file:///a/mod.ts",
"range": {
"start": { "line": 0, "character": 13 },
"end": { "line": 0, "character": 14 }
}
}, {
"uri": "file:///a/mod.test.ts",
"range": {
"start": { "line": 0, "character": 9 },
"end": { "line": 0, "character": 10 }
}
}, {
"uri": "file:///a/mod.test.ts",
"range": {
"start": { "line": 0, "character": 42 },
"end": { "line": 0, "character": 43 }
}
}])
);
// test 0 references
let res = client.write_request(
"textDocument/references",
json!({
"textDocument": {
"uri": "file:///a/mod.ts",
},
"position": { "line": 1, "character": 6 },
"context": {
"includeDeclaration": false
}
}),
);
assert_eq!(res, json!(null)); // seems it always returns null for this, which is ok
client.shutdown();
}
#[test]
fn lsp_signature_help() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "/**\n * Adds two numbers.\n * @param a This is a first number.\n * @param b This is a second number.\n */\nfunction add(a: number, b: number) {\n return a + b;\n}\n\nadd("
}
}),
);
let res = client.write_request(
"textDocument/signatureHelp",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": { "character": 4, "line": 9 },
"context": {
"triggerKind": 2,
"triggerCharacter": "(",
"isRetrigger": false
}
}),
);
assert_eq!(
res,
json!({
"signatures": [
{
"label": "add(a: number, b: number): number",
"documentation": {
"kind": "markdown",
"value": "Adds two numbers."
},
"parameters": [
{
"label": "a: number",
"documentation": {
"kind": "markdown",
"value": "This is a first number."
}
}, {
"label": "b: number",
"documentation": {
"kind": "markdown",
"value": "This is a second number."
}
}
]
}
],
"activeSignature": 0,
"activeParameter": 0
})
);
client.write_notification(
"textDocument/didChange",
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"version": 2
},
"contentChanges": [
{
"range": {
"start": { "line": 9, "character": 4 },
"end": { "line": 9, "character": 4 }
},
"text": "123, "
}
]
}),
);
let res = client.write_request(
"textDocument/signatureHelp",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": { "character": 8, "line": 9 }
}),
);
assert_eq!(
res,
json!({
"signatures": [
{
"label": "add(a: number, b: number): number",
"documentation": {
"kind": "markdown",
"value": "Adds two numbers."
},
"parameters": [
{
"label": "a: number",
"documentation": {
"kind": "markdown",
"value": "This is a first number."
}
}, {
"label": "b: number",
"documentation": {
"kind": "markdown",
"value": "This is a second number."
}
}
]
}
],
"activeSignature": 0,
"activeParameter": 1
})
);
client.shutdown();
}
#[test]
fn lsp_code_actions() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "export function a(): void {\n await Promise.resolve(\"a\");\n}\n\nexport function b(): void {\n await Promise.resolve(\"b\");\n}\n"
}
}),
);
let res = client
.write_request( "textDocument/codeAction",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"range": {
"start": { "line": 1, "character": 2 },
"end": { "line": 1, "character": 7 }
},
"context": {
"diagnostics": [{
"range": {
"start": { "line": 1, "character": 2 },
"end": { "line": 1, "character": 7 }
},
"severity": 1,
"code": 1308,
"source": "deno-ts",
"message": "'await' expressions are only allowed within async functions and at the top levels of modules.",
"relatedInformation": []
}],
"only": ["quickfix"]
}
}),
)
;
assert_eq!(
res,
json!([{
"title": "Add async modifier to containing function",
"kind": "quickfix",
"diagnostics": [{
"range": {
"start": { "line": 1, "character": 2 },
"end": { "line": 1, "character": 7 }
},
"severity": 1,
"code": 1308,
"source": "deno-ts",
"message": "'await' expressions are only allowed within async functions and at the top levels of modules.",
"relatedInformation": []
}],
"edit": {
"documentChanges": [{
"textDocument": {
"uri": "file:///a/file.ts",
"version": 1
},
"edits": [{
"range": {
"start": { "line": 0, "character": 7 },
"end": { "line": 0, "character": 7 }
},
"newText": "async "
}, {
"range": {
"start": { "line": 0, "character": 21 },
"end": { "line": 0, "character": 25 }
},
"newText": "Promise"
}]
}]
}
}, {
"title": "Add all missing 'async' modifiers",
"kind": "quickfix",
"diagnostics": [{
"range": {
"start": { "line": 1, "character": 2 },
"end": { "line": 1, "character": 7 }
},
"severity": 1,
"code": 1308,
"source": "deno-ts",
"message": "'await' expressions are only allowed within async functions and at the top levels of modules.",
"relatedInformation": []
}],
"data": {
"specifier": "file:///a/file.ts",
"fixId": "fixAwaitInSyncFunction"
}
}])
);
let res = client
.write_request(
"codeAction/resolve",
json!({
"title": "Add all missing 'async' modifiers",
"kind": "quickfix",
"diagnostics": [{
"range": {
"start": { "line": 1, "character": 2 },
"end": { "line": 1, "character": 7 }
},
"severity": 1,
"code": 1308,
"source": "deno-ts",
"message": "'await' expressions are only allowed within async functions and at the top levels of modules.",
"relatedInformation": []
}],
"data": {
"specifier": "file:///a/file.ts",
"fixId": "fixAwaitInSyncFunction"
}
}),
);
assert_eq!(
res,
json!({
"title": "Add all missing 'async' modifiers",
"kind": "quickfix",
"diagnostics": [
{
"range": {
"start": {
"line": 1,
"character": 2
},
"end": {
"line": 1,
"character": 7
}
},
"severity": 1,
"code": 1308,
"source": "deno-ts",
"message": "'await' expressions are only allowed within async functions and at the top levels of modules.",
"relatedInformation": []
}
],
"edit": {
"documentChanges": [{
"textDocument": {
"uri": "file:///a/file.ts",
"version": 1
},
"edits": [{
"range": {
"start": { "line": 0, "character": 7 },
"end": { "line": 0, "character": 7 }
},
"newText": "async "
}, {
"range": {
"start": { "line": 0, "character": 21 },
"end": { "line": 0, "character": 25 }
},
"newText": "Promise"
}, {
"range": {
"start": { "line": 4, "character": 7 },
"end": { "line": 4, "character": 7 }
},
"newText": "async "
}, {
"range": {
"start": { "line": 4, "character": 21 },
"end": { "line": 4, "character": 25 }
},
"newText": "Promise"
}]
}]
},
"data": {
"specifier": "file:///a/file.ts",
"fixId": "fixAwaitInSyncFunction"
}
})
);
client.shutdown();
}
#[test]
fn test_lsp_code_actions_ordering() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": r#"
import "https://deno.land/x/a/mod.ts";
let a = "a";
console.log(a);
export function b(): void {
await Promise.resolve("b");
}
"#
}
}));
let res = client.write_request(
"textDocument/codeAction",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"range": {
"start": { "line": 1, "character": 11 },
"end": { "line": 6, "character": 12 }
},
"context": {
"diagnostics": diagnostics.all(),
"only": ["quickfix"]
}
}),
);
// Simplify the serialization to `{ title, source }` for this test.
let mut actions: Vec = serde_json::from_value(res).unwrap();
for action in &mut actions {
let action = action.as_object_mut().unwrap();
let title = action.get("title").unwrap().as_str().unwrap().to_string();
let diagnostics = action.get("diagnostics").unwrap().as_array().unwrap();
let diagnostic = diagnostics.first().unwrap().as_object().unwrap();
let source = diagnostic.get("source").unwrap();
let source = source.as_str().unwrap().to_string();
action.clear();
action.insert("title".to_string(), serde_json::to_value(title).unwrap());
action.insert("source".to_string(), serde_json::to_value(source).unwrap());
}
let res = serde_json::to_value(actions).unwrap();
// Ensure ordering is "deno" -> "deno-ts" -> "deno-lint".
assert_eq!(
res,
json!([
{
"title": "Cache \"https://deno.land/x/a/mod.ts\" and its dependencies.",
"source": "deno",
},
{
"title": "Add async modifier to containing function",
"source": "deno-ts",
},
{
"title": "Disable prefer-const for this line",
"source": "deno-lint",
},
{
"title": "Disable prefer-const for the entire file",
"source": "deno-lint",
},
{
"title": "Ignore lint errors for the entire file",
"source": "deno-lint",
},
{
"title": "Disable no-await-in-sync-fn for this line",
"source": "deno-lint",
},
{
"title": "Disable no-await-in-sync-fn for the entire file",
"source": "deno-lint",
},
{
"title": "Ignore lint errors for the entire file",
"source": "deno-lint",
},
])
);
client.shutdown();
}
#[test]
fn lsp_status_file() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
let res = client.write_request(
"deno/virtualTextDocument",
json!({
"textDocument": {
"uri": "deno:/status.md"
}
}),
);
let res = res.as_str().unwrap().to_string();
assert!(res.starts_with("# Deno Language Server Status"));
let res = client.write_request(
"deno/virtualTextDocument",
json!({
"textDocument": {
"uri": "deno:/status.md?1"
}
}),
);
let res = res.as_str().unwrap().to_string();
assert!(res.starts_with("# Deno Language Server Status"));
client.shutdown();
}
#[test]
fn lsp_code_actions_deno_cache() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "import * as a from \"https://deno.land/x/a/mod.ts\";\n\nconsole.log(a);\n"
}
}));
assert_eq!(
diagnostics.messages_with_source("deno"),
serde_json::from_value(json!({
"uri": "file:///a/file.ts",
"diagnostics": [{
"range": {
"start": { "line": 0, "character": 19 },
"end": { "line": 0, "character": 49 }
},
"severity": 1,
"code": "no-cache",
"source": "deno",
"message": "Uncached or missing remote URL: https://deno.land/x/a/mod.ts",
"data": { "specifier": "https://deno.land/x/a/mod.ts" }
}],
"version": 1
})).unwrap()
);
let res =
client
.write_request(
"textDocument/codeAction",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"range": {
"start": { "line": 0, "character": 19 },
"end": { "line": 0, "character": 49 }
},
"context": {
"diagnostics": [{
"range": {
"start": { "line": 0, "character": 19 },
"end": { "line": 0, "character": 49 }
},
"severity": 1,
"code": "no-cache",
"source": "deno",
"message": "Unable to load the remote module: \"https://deno.land/x/a/mod.ts\".",
"data": {
"specifier": "https://deno.land/x/a/mod.ts"
}
}],
"only": ["quickfix"]
}
}),
);
assert_eq!(
res,
json!([{
"title": "Cache \"https://deno.land/x/a/mod.ts\" and its dependencies.",
"kind": "quickfix",
"diagnostics": [{
"range": {
"start": { "line": 0, "character": 19 },
"end": { "line": 0, "character": 49 }
},
"severity": 1,
"code": "no-cache",
"source": "deno",
"message": "Unable to load the remote module: \"https://deno.land/x/a/mod.ts\".",
"data": {
"specifier": "https://deno.land/x/a/mod.ts"
}
}],
"command": {
"title": "",
"command": "deno.cache",
"arguments": [["https://deno.land/x/a/mod.ts"], "file:///a/file.ts"]
}
}])
);
client.shutdown();
}
#[test]
fn lsp_code_actions_deno_cache_jsr() {
let context = TestContextBuilder::new()
.use_http_server()
.use_temp_cwd()
.build();
let temp_dir = context.temp_dir();
let mut client = context.new_lsp_command().build();
client.initialize_default();
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": r#"
import { add } from "jsr:@denotest/add@1";
console.log(add(1, 2));
"#,
},
}));
assert_eq!(
json!(diagnostics.messages_with_source("deno")),
json!({
"uri": temp_dir.url().join("file.ts").unwrap(),
"diagnostics": [{
"range": {
"start": { "line": 1, "character": 28 },
"end": { "line": 1, "character": 49 },
},
"severity": 1,
"code": "not-installed-jsr",
"source": "deno",
"message": "JSR package \"@denotest/add@1\" is not installed or doesn't exist.",
"data": { "specifier": "jsr:@denotest/add@1" },
}],
"version": 1,
})
);
let res = client.write_request(
"textDocument/codeAction",
json!({
"textDocument": { "uri": temp_dir.url().join("file.ts").unwrap() },
"range": {
"start": { "line": 1, "character": 28 },
"end": { "line": 1, "character": 49 },
},
"context": {
"diagnostics": [{
"range": {
"start": { "line": 1, "character": 28 },
"end": { "line": 1, "character": 49 },
},
"severity": 1,
"code": "not-installed-jsr",
"source": "deno",
"message": "JSR package \"@denotest/add@1\" is not installed or doesn't exist.",
"data": { "specifier": "jsr:@denotest/add@1" },
}],
"only": ["quickfix"],
}
}),
);
assert_eq!(
res,
json!([{
"title": "Install \"jsr:@denotest/add@1\" and its dependencies.",
"kind": "quickfix",
"diagnostics": [{
"range": {
"start": { "line": 1, "character": 28 },
"end": { "line": 1, "character": 49 },
},
"severity": 1,
"code": "not-installed-jsr",
"source": "deno",
"message": "JSR package \"@denotest/add@1\" is not installed or doesn't exist.",
"data": { "specifier": "jsr:@denotest/add@1" },
}],
"command": {
"title": "",
"command": "deno.cache",
"arguments": [
["jsr:@denotest/add@1"],
temp_dir.url().join("file.ts").unwrap(),
],
},
}])
);
client.write_request(
"workspace/executeCommand",
json!({
"command": "deno.cache",
"arguments": [
["jsr:@denotest/add@1"],
temp_dir.url().join("file.ts").unwrap(),
],
}),
);
let diagnostics = client.read_diagnostics();
assert_eq!(json!(diagnostics.all()), json!([]));
client.shutdown();
}
#[test]
fn lsp_jsr_lockfile() {
let context = TestContextBuilder::for_jsr().use_temp_cwd().build();
let temp_dir = context.temp_dir();
temp_dir.write("./deno.json", json!({}).to_string());
let lockfile = temp_dir.path().join("deno.lock");
let integrity = context.get_jsr_package_integrity("@denotest/add/0.2.0");
lockfile.write_json(&json!({
"version": "3",
"packages": {
"specifiers": {
// This is an old version of the package which exports `sum()` instead
// of `add()`.
"jsr:@denotest/add": "jsr:@denotest/add@0.2.0",
},
"jsr": {
"@denotest/add@0.2.0": {
"integrity": integrity
}
}
},
"remote": {},
}));
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": r#"
import { sum } from "jsr:@denotest/add";
console.log(sum(1, 2));
"#,
},
}));
client.write_request(
"workspace/executeCommand",
json!({
"command": "deno.cache",
"arguments": [
[],
temp_dir.url().join("file.ts").unwrap(),
],
}),
);
let diagnostics = client.read_diagnostics();
assert_eq!(json!(diagnostics.all()), json!([]));
client.shutdown();
}
#[test]
fn lsp_jsr_auto_import_completion() {
let context = TestContextBuilder::new()
.use_http_server()
.use_temp_cwd()
.build();
let temp_dir = context.temp_dir();
temp_dir.write(
"main.ts",
r#"
import "jsr:@denotest/add@1";
"#,
);
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.write_request(
"workspace/executeCommand",
json!({
"command": "deno.cache",
"arguments": [
[],
temp_dir.url().join("main.ts").unwrap(),
],
}),
);
client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": r#"add"#,
}
}));
let list = client.get_completion_list(
temp_dir.url().join("file.ts").unwrap(),
(0, 3),
json!({ "triggerKind": 1 }),
);
assert!(!list.is_incomplete);
assert_eq!(list.items.len(), 268);
let item = list.items.iter().find(|i| i.label == "add").unwrap();
assert_eq!(&item.label, "add");
assert_eq!(
json!(&item.label_details),
json!({ "description": "jsr:@denotest/add@1" })
);
let res = client.write_request("completionItem/resolve", json!(item));
assert_eq!(
res,
json!({
"label": "add",
"labelDetails": { "description": "jsr:@denotest/add@1" },
"kind": 3,
"detail": "function add(a: number, b: number): number",
"documentation": { "kind": "markdown", "value": "" },
"sortText": "\u{ffff}16_1",
"additionalTextEdits": [
{
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 0, "character": 0 },
},
"newText": "import { add } from \"jsr:@denotest/add@1\";\n\n",
},
],
})
);
client.shutdown();
}
#[test]
fn lsp_jsr_auto_import_completion_import_map() {
let context = TestContextBuilder::new()
.use_http_server()
.use_temp_cwd()
.build();
let temp_dir = context.temp_dir();
temp_dir.write(
"deno.json",
json!({
"imports": {
"add": "jsr:@denotest/add@^1.0",
},
})
.to_string(),
);
temp_dir.write(
"main.ts",
r#"
import "jsr:@denotest/add@1";
"#,
);
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.write_request(
"workspace/executeCommand",
json!({
"command": "deno.cache",
"arguments": [
[],
temp_dir.url().join("main.ts").unwrap(),
],
}),
);
client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": r#"add"#,
}
}));
let list = client.get_completion_list(
temp_dir.url().join("file.ts").unwrap(),
(0, 3),
json!({ "triggerKind": 1 }),
);
assert!(!list.is_incomplete);
assert_eq!(list.items.len(), 268);
let item = list.items.iter().find(|i| i.label == "add").unwrap();
assert_eq!(&item.label, "add");
assert_eq!(json!(&item.label_details), json!({ "description": "add" }));
let res = client.write_request("completionItem/resolve", json!(item));
assert_eq!(
res,
json!({
"label": "add",
"labelDetails": { "description": "add" },
"kind": 3,
"detail": "function add(a: number, b: number): number",
"documentation": { "kind": "markdown", "value": "" },
"sortText": "\u{ffff}16_0",
"additionalTextEdits": [
{
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 0, "character": 0 },
},
"newText": "import { add } from \"add\";\n\n",
},
],
})
);
client.shutdown();
}
#[test]
fn lsp_jsr_auto_import_completion_import_map_sub_path() {
let context = TestContextBuilder::new()
.use_http_server()
.use_temp_cwd()
.build();
let temp_dir = context.temp_dir();
temp_dir.write(
"deno.json",
json!({
"imports": {
"@std/path": "jsr:@std/path@^0.220.1",
},
})
.to_string(),
);
let file = source_file(
temp_dir.path().join("file.ts"),
r#"
// Adds jsr:@std/path@^0.220.1/normalize to the module graph.
import "jsr:@std/url@^0.220.1/normalize";
normalize
"#,
);
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.write_request(
"workspace/executeCommand",
json!({
"command": "deno.cache",
"arguments": [[], file.url()],
}),
);
client.read_diagnostics();
client.did_open_file(&file);
let list = client.get_completion_list(
file.url(),
(3, 15),
json!({ "triggerKind": 1 }),
);
let item = list
.items
.iter()
.find(|i| {
i.label == "normalize"
&& json!(&i.label_details)
.to_string()
.contains("\"@std/path/posix/normalize\"")
})
.unwrap();
let res = client.write_request("completionItem/resolve", json!(item));
assert_eq!(
res,
json!({
"label": "normalize",
"labelDetails": { "description": "@std/path/posix/normalize" },
"kind": 3,
"detail": "function normalize(path: string): string",
"documentation": { "kind": "markdown", "value": "Normalize the `path`, resolving `'..'` and `'.'` segments.\nNote that resolving these segments does not necessarily mean that all will be eliminated.\nA `'..'` at the top-level will be preserved, and an empty path is canonically `'.'`.\n\n*@param* - path to be normalized" },
"sortText": "\u{ffff}16_0",
"additionalTextEdits": [
{
"range": {
"start": { "line": 2, "character": 6 },
"end": { "line": 2, "character": 6 },
},
"newText": "import { normalize } from \"@std/path/posix/normalize\";\n",
},
],
}),
);
client.shutdown();
}
#[test]
fn lsp_jsr_code_action_missing_declaration() {
let context = TestContextBuilder::new()
.use_http_server()
.use_temp_cwd()
.build();
let temp_dir = context.temp_dir();
let file = source_file(
temp_dir.path().join("file.ts"),
r#"
import { someFunction } from "jsr:@denotest/types-file";
assertReturnType(someFunction());
"#,
);
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.write_request(
"workspace/executeCommand",
json!({
"command": "deno.cache",
"arguments": [[], file.url()],
}),
);
client.did_open_file(&file);
let res = client.write_request(
"textDocument/codeAction",
json!({
"textDocument": {
"uri": file.url(),
},
"range": {
"start": { "line": 2, "character": 6 },
"end": { "line": 2, "character": 22 },
},
"context": {
"diagnostics": [
{
"range": {
"start": { "line": 2, "character": 6 },
"end": { "line": 2, "character": 22 },
},
"severity": 8,
"code": 2304,
"source": "deno-ts",
"message": "Cannot find name 'assertReturnType'.",
"relatedInformation": [],
},
],
"only": ["quickfix"],
},
}),
);
assert_eq!(
res,
json!([
{
"title": "Add missing function declaration 'assertReturnType'",
"kind": "quickfix",
"diagnostics": [
{
"range": {
"start": {
"line": 2,
"character": 6,
},
"end": {
"line": 2,
"character": 22,
},
},
"severity": 8,
"code": 2304,
"source": "deno-ts",
"message": "Cannot find name 'assertReturnType'.",
"relatedInformation": [],
},
],
"edit": {
"documentChanges": [
{
"textDocument": {
"uri": file.url(),
"version": 1,
},
"edits": [
{
"range": {
"start": {
"line": 1,
"character": 6,
},
"end": {
"line": 1,
"character": 6,
},
},
"newText": "import { ReturnType } from \"jsr:@denotest/types-file/types\";\n",
},
{
"range": {
"start": {
"line": 3,
"character": 0,
},
"end": {
"line": 3,
"character": 0,
},
},
"newText": "\n function assertReturnType(arg0: ReturnType) {\n throw new Error(\"Function not implemented.\");\n }\n",
},
],
},
],
},
},
])
);
}
#[test]
fn lsp_jsr_code_action_move_to_new_file() {
let context = TestContextBuilder::new()
.use_http_server()
.use_temp_cwd()
.build();
let temp_dir = context.temp_dir();
let file = source_file(
temp_dir.path().join("file.ts"),
r#"
import { someFunction } from "jsr:@denotest/types-file";
export const someValue = someFunction();
"#,
);
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.write_request(
"workspace/executeCommand",
json!({
"command": "deno.cache",
"arguments": [[], file.url()],
}),
);
client.did_open_file(&file);
let list = client
.write_request_with_res_as::