mirror of
https://github.com/denoland/deno.git
synced 2024-11-21 15:04:11 -05:00
parent
c8e9b2654e
commit
301d3e4b68
38 changed files with 4878 additions and 47 deletions
66
Cargo.lock
generated
66
Cargo.lock
generated
|
@ -366,10 +366,20 @@ version = "0.4.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87"
|
checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils 0.7.2",
|
||||||
"maybe-uninit",
|
"maybe-uninit",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-channel"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
"crossbeam-utils 0.8.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
|
@ -381,6 +391,17 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg 1.0.1",
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
"lazy_static",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
|
@ -437,6 +458,7 @@ dependencies = [
|
||||||
"bytes 0.5.6",
|
"bytes 0.5.6",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
"crossbeam-channel 0.5.0",
|
||||||
"deno_core",
|
"deno_core",
|
||||||
"deno_crypto",
|
"deno_crypto",
|
||||||
"deno_doc",
|
"deno_doc",
|
||||||
|
@ -457,9 +479,12 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
|
"lsp-server",
|
||||||
|
"lsp-types",
|
||||||
"nix",
|
"nix",
|
||||||
"notify",
|
"notify",
|
||||||
"os_pipe",
|
"os_pipe",
|
||||||
|
"percent-encoding",
|
||||||
"regex",
|
"regex",
|
||||||
"ring",
|
"ring",
|
||||||
"rustyline",
|
"rustyline",
|
||||||
|
@ -1314,6 +1339,32 @@ dependencies = [
|
||||||
"cfg-if 0.1.10",
|
"cfg-if 0.1.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lsp-server"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69b18dfe0e4a380b872aa79d8e0ee6c3d7a9682466e84b83ad807c88b3545f79"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-channel 0.5.0",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lsp-types"
|
||||||
|
version = "0.84.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b95be71fe205e44de754185bcf86447b65813ce1ceb298f8d3793ade5fff08d"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.12.3",
|
||||||
|
"bitflags",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_repr",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matches"
|
name = "matches"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
|
@ -1513,7 +1564,7 @@ checksum = "77d03607cf88b4b160ba0e9ed425fff3cee3b55ac813f0c685b3a3772da37d0e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anymap",
|
"anymap",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel 0.4.4",
|
||||||
"filetime",
|
"filetime",
|
||||||
"fsevent",
|
"fsevent",
|
||||||
"fsevent-sys",
|
"fsevent-sys",
|
||||||
|
@ -2282,6 +2333,17 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_repr"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2dc6b7951b17b051f3210b063f12cc17320e2fe30ae05b0fe2a3abb068551c76"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2 1.0.24",
|
||||||
|
"quote 1.0.7",
|
||||||
|
"syn 1.0.48",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
|
|
|
@ -32,22 +32,24 @@ winres = "0.1.11"
|
||||||
winapi = "0.3.9"
|
winapi = "0.3.9"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
deno_crypto = { path = "../op_crates/crypto", version = "0.3.0" }
|
|
||||||
deno_core = { path = "../core", version = "0.69.0" }
|
deno_core = { path = "../core", version = "0.69.0" }
|
||||||
|
deno_crypto = { path = "../op_crates/crypto", version = "0.3.0" }
|
||||||
deno_doc = "0.1.17"
|
deno_doc = "0.1.17"
|
||||||
|
deno_fetch = { path = "../op_crates/fetch", version = "0.12.0" }
|
||||||
deno_lint = "0.2.12"
|
deno_lint = "0.2.12"
|
||||||
deno_web = { path = "../op_crates/web", version = "0.20.0" }
|
deno_web = { path = "../op_crates/web", version = "0.20.0" }
|
||||||
deno_fetch = { path = "../op_crates/fetch", version = "0.12.0" }
|
|
||||||
|
|
||||||
atty = "0.2.14"
|
atty = "0.2.14"
|
||||||
base64 = "0.12.3"
|
base64 = "0.12.3"
|
||||||
bytes = "0.5.6"
|
bytes = "0.5.6"
|
||||||
byteorder = "1.3.4"
|
byteorder = "1.3.4"
|
||||||
clap = "2.33.3"
|
clap = "2.33.3"
|
||||||
|
crossbeam-channel = "0.5.0"
|
||||||
dissimilar = "1.0.2"
|
dissimilar = "1.0.2"
|
||||||
dlopen = "0.1.8"
|
dlopen = "0.1.8"
|
||||||
encoding_rs = "0.8.24"
|
|
||||||
dprint-plugin-typescript = "0.35.0"
|
dprint-plugin-typescript = "0.35.0"
|
||||||
|
encoding_rs = "0.8.24"
|
||||||
|
env_logger = "0.7.1"
|
||||||
filetime = "0.2.12"
|
filetime = "0.2.12"
|
||||||
http = "0.2.1"
|
http = "0.2.1"
|
||||||
indexmap = "1.6.0"
|
indexmap = "1.6.0"
|
||||||
|
@ -55,31 +57,33 @@ jsonc-parser = "0.14.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
libc = "0.2.77"
|
libc = "0.2.77"
|
||||||
log = "0.4.11"
|
log = "0.4.11"
|
||||||
env_logger = "0.7.1"
|
lsp-server = "0.5.0"
|
||||||
|
lsp-types = { version = "0.84.0", features = ["proposed"] }
|
||||||
notify = "5.0.0-pre.3"
|
notify = "5.0.0-pre.3"
|
||||||
|
percent-encoding = "2.1.0"
|
||||||
regex = "1.3.9"
|
regex = "1.3.9"
|
||||||
ring = "0.16.15"
|
ring = "0.16.15"
|
||||||
rustyline = { version = "7.0.0", default-features = false }
|
rustyline = { version = "7.0.0", default-features = false }
|
||||||
rustyline-derive = "0.4.0"
|
rustyline-derive = "0.4.0"
|
||||||
|
semver-parser = "0.9.0"
|
||||||
serde = { version = "1.0.116", features = ["derive"] }
|
serde = { version = "1.0.116", features = ["derive"] }
|
||||||
shell-escape = "0.1.5"
|
shell-escape = "0.1.5"
|
||||||
sys-info = "0.7.0"
|
|
||||||
sourcemap = "6.0.1"
|
sourcemap = "6.0.1"
|
||||||
swc_bundler = "0.17.5"
|
swc_bundler = "0.17.5"
|
||||||
swc_common = { version = "0.10.6", features = ["sourcemap"] }
|
swc_common = { version = "0.10.6", features = ["sourcemap"] }
|
||||||
swc_ecmascript = { version = "0.14.4", features = ["codegen", "dep_graph", "parser", "react", "transforms", "visit"] }
|
swc_ecmascript = { version = "0.14.4", features = ["codegen", "dep_graph", "parser", "react", "transforms", "visit"] }
|
||||||
|
sys-info = "0.7.0"
|
||||||
tempfile = "3.1.0"
|
tempfile = "3.1.0"
|
||||||
termcolor = "1.1.0"
|
termcolor = "1.1.0"
|
||||||
tokio = { version = "0.2.22", features = ["full"] }
|
tokio = { version = "0.2.22", features = ["full"] }
|
||||||
tokio-rustls = "0.14.1"
|
tokio-rustls = "0.14.1"
|
||||||
# Keep in-sync with warp.
|
# Keep in-sync with warp.
|
||||||
tokio-tungstenite = "0.11.0"
|
tokio-tungstenite = "0.11.0"
|
||||||
webpki = "0.21.3"
|
uuid = { version = "0.8.1", features = ["v4"] }
|
||||||
webpki-roots = "=0.19.0" # Pinned to v0.19.0 to match 'reqwest'.
|
|
||||||
walkdir = "2.3.1"
|
walkdir = "2.3.1"
|
||||||
warp = { version = "0.2.5", features = ["tls"] }
|
warp = { version = "0.2.5", features = ["tls"] }
|
||||||
semver-parser = "0.9.0"
|
webpki = "0.21.3"
|
||||||
uuid = { version = "0.8.1", features = ["v4"] }
|
webpki-roots = "=0.19.0" # Pinned to v0.19.0 to match 'reqwest'.
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winapi = { version = "0.3.9", features = ["knownfolders", "mswsock", "objbase", "shlobj", "tlhelp32", "winbase", "winerror", "winsock2"] }
|
winapi = { version = "0.3.9", features = ["knownfolders", "mswsock", "objbase", "shlobj", "tlhelp32", "winbase", "winerror", "winsock2"] }
|
||||||
|
|
|
@ -354,7 +354,7 @@ impl ParsedModule {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_with_source_map(
|
pub fn parse_with_source_map(
|
||||||
specifier: &str,
|
specifier: &str,
|
||||||
source: &str,
|
source: &str,
|
||||||
media_type: &MediaType,
|
media_type: &MediaType,
|
||||||
|
|
|
@ -131,7 +131,7 @@ fn fetch_local(specifier: &ModuleSpecifier) -> Result<File, AnyError> {
|
||||||
|
|
||||||
/// Given a vector of bytes and optionally a charset, decode the bytes to a
|
/// Given a vector of bytes and optionally a charset, decode the bytes to a
|
||||||
/// string.
|
/// string.
|
||||||
fn get_source_from_bytes(
|
pub fn get_source_from_bytes(
|
||||||
bytes: Vec<u8>,
|
bytes: Vec<u8>,
|
||||||
maybe_charset: Option<String>,
|
maybe_charset: Option<String>,
|
||||||
) -> Result<String, AnyError> {
|
) -> Result<String, AnyError> {
|
||||||
|
@ -161,7 +161,7 @@ fn get_validated_scheme(
|
||||||
|
|
||||||
/// Resolve a media type and optionally the charset from a module specifier and
|
/// Resolve a media type and optionally the charset from a module specifier and
|
||||||
/// the value of a content type header.
|
/// the value of a content type header.
|
||||||
fn map_content_type(
|
pub fn map_content_type(
|
||||||
specifier: &ModuleSpecifier,
|
specifier: &ModuleSpecifier,
|
||||||
maybe_content_type: Option<String>,
|
maybe_content_type: Option<String>,
|
||||||
) -> (MediaType, Option<String>) {
|
) -> (MediaType, Option<String>) {
|
||||||
|
|
38
cli/flags.rs
38
cli/flags.rs
|
@ -17,6 +17,9 @@ pub enum DenoSubcommand {
|
||||||
source_file: String,
|
source_file: String,
|
||||||
out_file: Option<PathBuf>,
|
out_file: Option<PathBuf>,
|
||||||
},
|
},
|
||||||
|
Cache {
|
||||||
|
files: Vec<String>,
|
||||||
|
},
|
||||||
Compile {
|
Compile {
|
||||||
source_file: String,
|
source_file: String,
|
||||||
output: Option<PathBuf>,
|
output: Option<PathBuf>,
|
||||||
|
@ -35,9 +38,6 @@ pub enum DenoSubcommand {
|
||||||
code: String,
|
code: String,
|
||||||
as_typescript: bool,
|
as_typescript: bool,
|
||||||
},
|
},
|
||||||
Cache {
|
|
||||||
files: Vec<String>,
|
|
||||||
},
|
|
||||||
Fmt {
|
Fmt {
|
||||||
check: bool,
|
check: bool,
|
||||||
files: Vec<PathBuf>,
|
files: Vec<PathBuf>,
|
||||||
|
@ -54,6 +54,7 @@ pub enum DenoSubcommand {
|
||||||
root: Option<PathBuf>,
|
root: Option<PathBuf>,
|
||||||
force: bool,
|
force: bool,
|
||||||
},
|
},
|
||||||
|
LanguageServer,
|
||||||
Lint {
|
Lint {
|
||||||
files: Vec<PathBuf>,
|
files: Vec<PathBuf>,
|
||||||
ignore: Vec<PathBuf>,
|
ignore: Vec<PathBuf>,
|
||||||
|
@ -293,6 +294,8 @@ pub fn flags_from_vec_safe(args: Vec<String>) -> clap::Result<Flags> {
|
||||||
lint_parse(&mut flags, m);
|
lint_parse(&mut flags, m);
|
||||||
} else if let Some(m) = matches.subcommand_matches("compile") {
|
} else if let Some(m) = matches.subcommand_matches("compile") {
|
||||||
compile_parse(&mut flags, m);
|
compile_parse(&mut flags, m);
|
||||||
|
} else if let Some(m) = matches.subcommand_matches("lsp") {
|
||||||
|
language_server_parse(&mut flags, m);
|
||||||
} else {
|
} else {
|
||||||
repl_parse(&mut flags, &matches);
|
repl_parse(&mut flags, &matches);
|
||||||
}
|
}
|
||||||
|
@ -349,6 +352,7 @@ If the flag is set, restrict these messages to errors.",
|
||||||
.subcommand(fmt_subcommand())
|
.subcommand(fmt_subcommand())
|
||||||
.subcommand(info_subcommand())
|
.subcommand(info_subcommand())
|
||||||
.subcommand(install_subcommand())
|
.subcommand(install_subcommand())
|
||||||
|
.subcommand(language_server_subcommand())
|
||||||
.subcommand(lint_subcommand())
|
.subcommand(lint_subcommand())
|
||||||
.subcommand(repl_subcommand())
|
.subcommand(repl_subcommand())
|
||||||
.subcommand(run_subcommand())
|
.subcommand(run_subcommand())
|
||||||
|
@ -685,6 +689,10 @@ fn doc_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn language_server_parse(flags: &mut Flags, _matches: &clap::ArgMatches) {
|
||||||
|
flags.subcommand = DenoSubcommand::LanguageServer;
|
||||||
|
}
|
||||||
|
|
||||||
fn lint_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
|
fn lint_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
|
||||||
let files = match matches.values_of("files") {
|
let files = match matches.values_of("files") {
|
||||||
Some(f) => f.map(PathBuf::from).collect(),
|
Some(f) => f.map(PathBuf::from).collect(),
|
||||||
|
@ -1076,6 +1084,18 @@ Show documentation for runtime built-ins:
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn language_server_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||||
|
SubCommand::with_name("lsp")
|
||||||
|
.setting(AppSettings::Hidden)
|
||||||
|
.about("Start the language server")
|
||||||
|
.long_about(
|
||||||
|
r#"Start the Deno language server which will take input
|
||||||
|
from stdin and provide output to stdout.
|
||||||
|
deno lsp
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn lint_subcommand<'a, 'b>() -> App<'a, 'b> {
|
fn lint_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||||
SubCommand::with_name("lint")
|
SubCommand::with_name("lint")
|
||||||
.about("Lint source files")
|
.about("Lint source files")
|
||||||
|
@ -1952,6 +1972,18 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn language_server() {
|
||||||
|
let r = flags_from_vec_safe(svec!["deno", "lsp"]);
|
||||||
|
assert_eq!(
|
||||||
|
r.unwrap(),
|
||||||
|
Flags {
|
||||||
|
subcommand: DenoSubcommand::LanguageServer,
|
||||||
|
..Flags::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lint() {
|
fn lint() {
|
||||||
let r = flags_from_vec_safe(svec![
|
let r = flags_from_vec_safe(svec![
|
||||||
|
|
|
@ -72,7 +72,7 @@ pub fn url_to_filename(url: &Url) -> PathBuf {
|
||||||
cache_filename
|
cache_filename
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct HttpCache {
|
pub struct HttpCache {
|
||||||
pub location: PathBuf,
|
pub location: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
23
cli/lsp/README.md
Normal file
23
cli/lsp/README.md
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Deno Language Server
|
||||||
|
|
||||||
|
The Deno Language Server provides a server implementation of the
|
||||||
|
[Language Server Protocol](https://microsoft.github.io/language-server-protocol/)
|
||||||
|
which is specifically tailored to provide a _Deno_ view of code. It is
|
||||||
|
integrated into the command line and can be started via the `lsp` sub-command.
|
||||||
|
|
||||||
|
> :warning: The Language Server is highly experimental and far from feature
|
||||||
|
> complete.
|
||||||
|
|
||||||
|
This document gives an overview of the structure of the language server.
|
||||||
|
|
||||||
|
## Acknowledgement
|
||||||
|
|
||||||
|
The structure of the language server was heavily influenced and adapted from
|
||||||
|
[`rust-analyzer`](https://rust-analyzer.github.io/).
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
When the language server is started, a `ServerState` instance is created which
|
||||||
|
holds all the state of the language server, as well as provides the
|
||||||
|
infrastructure for receiving and sending notifications and requests from a
|
||||||
|
language server client.
|
324
cli/lsp/analysis.rs
Normal file
324
cli/lsp/analysis.rs
Normal file
|
@ -0,0 +1,324 @@
|
||||||
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
use crate::ast;
|
||||||
|
use crate::import_map::ImportMap;
|
||||||
|
use crate::media_type::MediaType;
|
||||||
|
use crate::module_graph::parse_deno_types;
|
||||||
|
use crate::module_graph::parse_ts_reference;
|
||||||
|
use crate::module_graph::TypeScriptReference;
|
||||||
|
use crate::tools::lint::create_linter;
|
||||||
|
|
||||||
|
use deno_core::error::AnyError;
|
||||||
|
use deno_core::ModuleSpecifier;
|
||||||
|
use deno_lint::rules;
|
||||||
|
use lsp_types::Position;
|
||||||
|
use lsp_types::Range;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// Category of self-generated diagnostic messages (those not coming from)
|
||||||
|
/// TypeScript.
|
||||||
|
pub enum Category {
|
||||||
|
/// A lint diagnostic, where the first element is the message.
|
||||||
|
Lint {
|
||||||
|
message: String,
|
||||||
|
code: String,
|
||||||
|
hint: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A structure to hold a reference to a diagnostic message.
|
||||||
|
pub struct Reference {
|
||||||
|
category: Category,
|
||||||
|
range: Range,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_lsp_range(range: &deno_lint::diagnostic::Range) -> Range {
|
||||||
|
Range {
|
||||||
|
start: Position {
|
||||||
|
line: (range.start.line - 1) as u32,
|
||||||
|
character: range.start.col as u32,
|
||||||
|
},
|
||||||
|
end: Position {
|
||||||
|
line: (range.end.line - 1) as u32,
|
||||||
|
character: range.end.col as u32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_lint_references(
|
||||||
|
specifier: &ModuleSpecifier,
|
||||||
|
media_type: &MediaType,
|
||||||
|
source_code: &str,
|
||||||
|
) -> Result<Vec<Reference>, AnyError> {
|
||||||
|
let syntax = ast::get_syntax(media_type);
|
||||||
|
let lint_rules = rules::get_recommended_rules();
|
||||||
|
let mut linter = create_linter(syntax, lint_rules);
|
||||||
|
// TODO(@kitsonk) we should consider caching the swc source file versions for
|
||||||
|
// reuse by other processes
|
||||||
|
let (_, lint_diagnostics) =
|
||||||
|
linter.lint(specifier.to_string(), source_code.to_string())?;
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
lint_diagnostics
|
||||||
|
.into_iter()
|
||||||
|
.map(|d| Reference {
|
||||||
|
category: Category::Lint {
|
||||||
|
message: d.message,
|
||||||
|
code: d.code,
|
||||||
|
hint: d.hint,
|
||||||
|
},
|
||||||
|
range: as_lsp_range(&d.range),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn references_to_diagnostics(
|
||||||
|
references: Vec<Reference>,
|
||||||
|
) -> Vec<lsp_types::Diagnostic> {
|
||||||
|
references
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| match r.category {
|
||||||
|
Category::Lint { message, code, .. } => lsp_types::Diagnostic {
|
||||||
|
range: r.range,
|
||||||
|
severity: Some(lsp_types::DiagnosticSeverity::Warning),
|
||||||
|
code: Some(lsp_types::NumberOrString::String(code)),
|
||||||
|
code_description: None,
|
||||||
|
// TODO(@kitsonk) this won't make sense for every diagnostic
|
||||||
|
source: Some("deno-lint".to_string()),
|
||||||
|
message,
|
||||||
|
related_information: None,
|
||||||
|
tags: None, // we should tag unused code
|
||||||
|
data: None,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Dependency {
|
||||||
|
pub is_dynamic: bool,
|
||||||
|
pub maybe_code: Option<ResolvedImport>,
|
||||||
|
pub maybe_type: Option<ResolvedImport>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum ResolvedImport {
|
||||||
|
Resolved(ModuleSpecifier),
|
||||||
|
Err(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_import(
|
||||||
|
specifier: &str,
|
||||||
|
referrer: &ModuleSpecifier,
|
||||||
|
maybe_import_map: Option<Rc<RefCell<ImportMap>>>,
|
||||||
|
) -> ResolvedImport {
|
||||||
|
let maybe_mapped = if let Some(import_map) = maybe_import_map {
|
||||||
|
if let Ok(maybe_specifier) =
|
||||||
|
import_map.borrow().resolve(specifier, referrer.as_str())
|
||||||
|
{
|
||||||
|
maybe_specifier
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let remapped = maybe_mapped.is_some();
|
||||||
|
let specifier = if let Some(remapped) = maybe_mapped {
|
||||||
|
remapped
|
||||||
|
} else {
|
||||||
|
match ModuleSpecifier::resolve_import(specifier, referrer.as_str()) {
|
||||||
|
Ok(resolved) => resolved,
|
||||||
|
Err(err) => return ResolvedImport::Err(err.to_string()),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let referrer_scheme = referrer.as_url().scheme();
|
||||||
|
let specifier_scheme = specifier.as_url().scheme();
|
||||||
|
if referrer_scheme == "https" && specifier_scheme == "http" {
|
||||||
|
return ResolvedImport::Err(
|
||||||
|
"Modules imported via https are not allowed to import http modules."
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (referrer_scheme == "https" || referrer_scheme == "http")
|
||||||
|
&& !(specifier_scheme == "https" || specifier_scheme == "http")
|
||||||
|
&& !remapped
|
||||||
|
{
|
||||||
|
return ResolvedImport::Err("Remote modules are not allowed to import local modules. Consider using a dynamic import instead.".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
ResolvedImport::Resolved(specifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(@kitsonk) a lot of this logic is duplicated in module_graph.rs in
|
||||||
|
// Module::parse() and should be refactored out to a common function.
|
||||||
|
pub fn analyze_dependencies(
|
||||||
|
specifier: &ModuleSpecifier,
|
||||||
|
source: &str,
|
||||||
|
media_type: &MediaType,
|
||||||
|
maybe_import_map: Option<Rc<RefCell<ImportMap>>>,
|
||||||
|
) -> Option<(HashMap<String, Dependency>, Option<ResolvedImport>)> {
|
||||||
|
let specifier_str = specifier.to_string();
|
||||||
|
let source_map = Rc::new(swc_common::SourceMap::default());
|
||||||
|
let mut maybe_type = None;
|
||||||
|
if let Ok(parsed_module) =
|
||||||
|
ast::parse_with_source_map(&specifier_str, source, &media_type, source_map)
|
||||||
|
{
|
||||||
|
let mut dependencies = HashMap::<String, Dependency>::new();
|
||||||
|
|
||||||
|
// Parse leading comments for supported triple slash references.
|
||||||
|
for comment in parsed_module.get_leading_comments().iter() {
|
||||||
|
if let Some(ts_reference) = parse_ts_reference(&comment.text) {
|
||||||
|
match ts_reference {
|
||||||
|
TypeScriptReference::Path(import) => {
|
||||||
|
let dep = dependencies.entry(import.clone()).or_default();
|
||||||
|
let resolved_import =
|
||||||
|
resolve_import(&import, specifier, maybe_import_map.clone());
|
||||||
|
dep.maybe_code = Some(resolved_import);
|
||||||
|
}
|
||||||
|
TypeScriptReference::Types(import) => {
|
||||||
|
let resolved_import =
|
||||||
|
resolve_import(&import, specifier, maybe_import_map.clone());
|
||||||
|
if media_type == &MediaType::JavaScript
|
||||||
|
|| media_type == &MediaType::JSX
|
||||||
|
{
|
||||||
|
maybe_type = Some(resolved_import)
|
||||||
|
} else {
|
||||||
|
let dep = dependencies.entry(import).or_default();
|
||||||
|
dep.maybe_type = Some(resolved_import);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse ES and type only imports
|
||||||
|
let descriptors = parsed_module.analyze_dependencies();
|
||||||
|
for desc in descriptors.into_iter().filter(|desc| {
|
||||||
|
desc.kind != swc_ecmascript::dep_graph::DependencyKind::Require
|
||||||
|
}) {
|
||||||
|
let resolved_import =
|
||||||
|
resolve_import(&desc.specifier, specifier, maybe_import_map.clone());
|
||||||
|
|
||||||
|
// Check for `@deno-types` pragmas that effect the import
|
||||||
|
let maybe_resolved_type_import =
|
||||||
|
if let Some(comment) = desc.leading_comments.last() {
|
||||||
|
if let Some(deno_types) = parse_deno_types(&comment.text).as_ref() {
|
||||||
|
Some(resolve_import(
|
||||||
|
deno_types,
|
||||||
|
specifier,
|
||||||
|
maybe_import_map.clone(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let dep = dependencies.entry(desc.specifier.to_string()).or_default();
|
||||||
|
dep.is_dynamic = desc.is_dynamic;
|
||||||
|
match desc.kind {
|
||||||
|
swc_ecmascript::dep_graph::DependencyKind::ExportType
|
||||||
|
| swc_ecmascript::dep_graph::DependencyKind::ImportType => {
|
||||||
|
dep.maybe_type = Some(resolved_import)
|
||||||
|
}
|
||||||
|
_ => dep.maybe_code = Some(resolved_import),
|
||||||
|
}
|
||||||
|
if maybe_resolved_type_import.is_some() && dep.maybe_type.is_none() {
|
||||||
|
dep.maybe_type = maybe_resolved_type_import;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((dependencies, maybe_type))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_as_lsp_range() {
|
||||||
|
let fixture = deno_lint::diagnostic::Range {
|
||||||
|
start: deno_lint::diagnostic::Position {
|
||||||
|
line: 1,
|
||||||
|
col: 2,
|
||||||
|
byte_pos: 23,
|
||||||
|
},
|
||||||
|
end: deno_lint::diagnostic::Position {
|
||||||
|
line: 2,
|
||||||
|
col: 0,
|
||||||
|
byte_pos: 33,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let actual = as_lsp_range(&fixture);
|
||||||
|
assert_eq!(
|
||||||
|
actual,
|
||||||
|
lsp_types::Range {
|
||||||
|
start: lsp_types::Position {
|
||||||
|
line: 0,
|
||||||
|
character: 2,
|
||||||
|
},
|
||||||
|
end: lsp_types::Position {
|
||||||
|
line: 1,
|
||||||
|
character: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_analyze_dependencies() {
|
||||||
|
let specifier =
|
||||||
|
ModuleSpecifier::resolve_url("file:///a.ts").expect("bad specifier");
|
||||||
|
let source = r#"import {
|
||||||
|
Application,
|
||||||
|
Context,
|
||||||
|
Router,
|
||||||
|
Status,
|
||||||
|
} from "https://deno.land/x/oak@v6.3.2/mod.ts";
|
||||||
|
|
||||||
|
// @deno-types="https://deno.land/x/types/react/index.d.ts";
|
||||||
|
import * as React from "https://cdn.skypack.dev/react";
|
||||||
|
"#;
|
||||||
|
let actual =
|
||||||
|
analyze_dependencies(&specifier, source, &MediaType::TypeScript, None);
|
||||||
|
assert!(actual.is_some());
|
||||||
|
let (actual, maybe_type) = actual.unwrap();
|
||||||
|
assert!(maybe_type.is_none());
|
||||||
|
assert_eq!(actual.len(), 2);
|
||||||
|
assert_eq!(
|
||||||
|
actual.get("https://cdn.skypack.dev/react").cloned(),
|
||||||
|
Some(Dependency {
|
||||||
|
is_dynamic: false,
|
||||||
|
maybe_code: Some(ResolvedImport::Resolved(
|
||||||
|
ModuleSpecifier::resolve_url("https://cdn.skypack.dev/react")
|
||||||
|
.unwrap()
|
||||||
|
)),
|
||||||
|
maybe_type: Some(ResolvedImport::Resolved(
|
||||||
|
ModuleSpecifier::resolve_url(
|
||||||
|
"https://deno.land/x/types/react/index.d.ts"
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
actual.get("https://deno.land/x/oak@v6.3.2/mod.ts").cloned(),
|
||||||
|
Some(Dependency {
|
||||||
|
is_dynamic: false,
|
||||||
|
maybe_code: Some(ResolvedImport::Resolved(
|
||||||
|
ModuleSpecifier::resolve_url("https://deno.land/x/oak@v6.3.2/mod.ts")
|
||||||
|
.unwrap()
|
||||||
|
)),
|
||||||
|
maybe_type: None,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
59
cli/lsp/capabilities.rs
Normal file
59
cli/lsp/capabilities.rs
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
///!
|
||||||
|
///! Provides information about what capabilities that are supported by the
|
||||||
|
///! language server, which helps determine what messages are sent from the
|
||||||
|
///! client.
|
||||||
|
///!
|
||||||
|
use lsp_types::ClientCapabilities;
|
||||||
|
use lsp_types::HoverProviderCapability;
|
||||||
|
use lsp_types::OneOf;
|
||||||
|
use lsp_types::SaveOptions;
|
||||||
|
use lsp_types::ServerCapabilities;
|
||||||
|
use lsp_types::TextDocumentSyncCapability;
|
||||||
|
use lsp_types::TextDocumentSyncKind;
|
||||||
|
use lsp_types::TextDocumentSyncOptions;
|
||||||
|
|
||||||
|
pub fn server_capabilities(
|
||||||
|
_client_capabilities: &ClientCapabilities,
|
||||||
|
) -> ServerCapabilities {
|
||||||
|
ServerCapabilities {
|
||||||
|
text_document_sync: Some(TextDocumentSyncCapability::Options(
|
||||||
|
TextDocumentSyncOptions {
|
||||||
|
open_close: Some(true),
|
||||||
|
change: Some(TextDocumentSyncKind::Incremental),
|
||||||
|
will_save: None,
|
||||||
|
will_save_wait_until: None,
|
||||||
|
save: Some(SaveOptions::default().into()),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
hover_provider: Some(HoverProviderCapability::Simple(true)),
|
||||||
|
completion_provider: None,
|
||||||
|
signature_help_provider: None,
|
||||||
|
declaration_provider: None,
|
||||||
|
definition_provider: Some(OneOf::Left(true)),
|
||||||
|
type_definition_provider: None,
|
||||||
|
implementation_provider: None,
|
||||||
|
references_provider: Some(OneOf::Left(true)),
|
||||||
|
document_highlight_provider: Some(OneOf::Left(true)),
|
||||||
|
document_symbol_provider: None,
|
||||||
|
workspace_symbol_provider: None,
|
||||||
|
code_action_provider: None,
|
||||||
|
code_lens_provider: None,
|
||||||
|
document_formatting_provider: Some(OneOf::Left(true)),
|
||||||
|
document_range_formatting_provider: None,
|
||||||
|
document_on_type_formatting_provider: None,
|
||||||
|
selection_range_provider: None,
|
||||||
|
semantic_highlighting: None,
|
||||||
|
folding_range_provider: None,
|
||||||
|
rename_provider: None,
|
||||||
|
document_link_provider: None,
|
||||||
|
color_provider: None,
|
||||||
|
execute_command_provider: None,
|
||||||
|
workspace: None,
|
||||||
|
call_hierarchy_provider: None,
|
||||||
|
semantic_tokens_provider: None,
|
||||||
|
on_type_rename_provider: None,
|
||||||
|
experimental: None,
|
||||||
|
}
|
||||||
|
}
|
49
cli/lsp/config.rs
Normal file
49
cli/lsp/config.rs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
use deno_core::error::AnyError;
|
||||||
|
use deno_core::serde::Deserialize;
|
||||||
|
use deno_core::serde_json;
|
||||||
|
use deno_core::serde_json::Value;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ClientCapabilities {
|
||||||
|
pub status_notification: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct WorkspaceSettings {
|
||||||
|
pub enable: bool,
|
||||||
|
pub config: Option<String>,
|
||||||
|
pub import_map: Option<String>,
|
||||||
|
pub lint: bool,
|
||||||
|
pub unstable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct Config {
|
||||||
|
pub client_capabilities: ClientCapabilities,
|
||||||
|
pub settings: WorkspaceSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn update(&mut self, value: Value) -> Result<(), AnyError> {
|
||||||
|
let settings: WorkspaceSettings = serde_json::from_value(value)?;
|
||||||
|
self.settings = settings;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::redundant_closure_call)]
|
||||||
|
pub fn update_capabilities(
|
||||||
|
&mut self,
|
||||||
|
capabilities: &lsp_types::ClientCapabilities,
|
||||||
|
) {
|
||||||
|
if let Some(experimental) = &capabilities.experimental {
|
||||||
|
let get_bool =
|
||||||
|
|k: &str| experimental.get(k).and_then(|it| it.as_bool()) == Some(true);
|
||||||
|
|
||||||
|
self.client_capabilities.status_notification =
|
||||||
|
get_bool("statusNotification");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
268
cli/lsp/diagnostics.rs
Normal file
268
cli/lsp/diagnostics.rs
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
use super::analysis::get_lint_references;
|
||||||
|
use super::analysis::references_to_diagnostics;
|
||||||
|
use super::memory_cache::FileId;
|
||||||
|
use super::state::ServerStateSnapshot;
|
||||||
|
use super::tsc;
|
||||||
|
|
||||||
|
use crate::diagnostics;
|
||||||
|
use crate::media_type::MediaType;
|
||||||
|
|
||||||
|
use deno_core::error::AnyError;
|
||||||
|
use deno_core::serde_json;
|
||||||
|
use deno_core::serde_json::Value;
|
||||||
|
use deno_core::url::Url;
|
||||||
|
use deno_core::JsRuntime;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
impl<'a> From<&'a diagnostics::DiagnosticCategory>
|
||||||
|
for lsp_types::DiagnosticSeverity
|
||||||
|
{
|
||||||
|
fn from(category: &'a diagnostics::DiagnosticCategory) -> Self {
|
||||||
|
match category {
|
||||||
|
diagnostics::DiagnosticCategory::Error => {
|
||||||
|
lsp_types::DiagnosticSeverity::Error
|
||||||
|
}
|
||||||
|
diagnostics::DiagnosticCategory::Warning => {
|
||||||
|
lsp_types::DiagnosticSeverity::Warning
|
||||||
|
}
|
||||||
|
diagnostics::DiagnosticCategory::Suggestion => {
|
||||||
|
lsp_types::DiagnosticSeverity::Hint
|
||||||
|
}
|
||||||
|
diagnostics::DiagnosticCategory::Message => {
|
||||||
|
lsp_types::DiagnosticSeverity::Information
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a diagnostics::Position> for lsp_types::Position {
|
||||||
|
fn from(pos: &'a diagnostics::Position) -> Self {
|
||||||
|
Self {
|
||||||
|
line: pos.line as u32,
|
||||||
|
character: pos.character as u32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_lsp_range(
|
||||||
|
start: &diagnostics::Position,
|
||||||
|
end: &diagnostics::Position,
|
||||||
|
) -> lsp_types::Range {
|
||||||
|
lsp_types::Range {
|
||||||
|
start: start.into(),
|
||||||
|
end: end.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||||
|
pub enum DiagnosticSource {
|
||||||
|
Lint,
|
||||||
|
TypeScript,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct DiagnosticCollection {
|
||||||
|
map: HashMap<(FileId, DiagnosticSource), Vec<lsp_types::Diagnostic>>,
|
||||||
|
versions: HashMap<FileId, i32>,
|
||||||
|
changes: HashSet<FileId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiagnosticCollection {
|
||||||
|
pub fn set(
|
||||||
|
&mut self,
|
||||||
|
file_id: FileId,
|
||||||
|
source: DiagnosticSource,
|
||||||
|
version: Option<i32>,
|
||||||
|
diagnostics: Vec<lsp_types::Diagnostic>,
|
||||||
|
) {
|
||||||
|
self.map.insert((file_id, source), diagnostics);
|
||||||
|
if let Some(version) = version {
|
||||||
|
self.versions.insert(file_id, version);
|
||||||
|
}
|
||||||
|
self.changes.insert(file_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn diagnostics_for(
|
||||||
|
&self,
|
||||||
|
file_id: FileId,
|
||||||
|
source: DiagnosticSource,
|
||||||
|
) -> impl Iterator<Item = &lsp_types::Diagnostic> {
|
||||||
|
self.map.get(&(file_id, source)).into_iter().flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_version(&self, file_id: &FileId) -> Option<i32> {
|
||||||
|
self.versions.get(file_id).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_changes(&mut self) -> Option<HashSet<FileId>> {
|
||||||
|
if self.changes.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(mem::take(&mut self.changes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type DiagnosticVec = Vec<(FileId, Option<i32>, Vec<lsp_types::Diagnostic>)>;
|
||||||
|
|
||||||
|
pub fn generate_linting_diagnostics(
|
||||||
|
state: &ServerStateSnapshot,
|
||||||
|
) -> DiagnosticVec {
|
||||||
|
if !state.config.settings.lint {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let mut diagnostics = Vec::new();
|
||||||
|
let file_cache = state.file_cache.read().unwrap();
|
||||||
|
for (specifier, doc_data) in state.doc_data.iter() {
|
||||||
|
let file_id = file_cache.lookup(specifier).unwrap();
|
||||||
|
let version = doc_data.version;
|
||||||
|
let current_version = state.diagnostics.get_version(&file_id);
|
||||||
|
if version != current_version {
|
||||||
|
let media_type = MediaType::from(specifier);
|
||||||
|
if let Ok(source_code) = file_cache.get_contents(file_id) {
|
||||||
|
if let Ok(references) =
|
||||||
|
get_lint_references(specifier, &media_type, &source_code)
|
||||||
|
{
|
||||||
|
if !references.is_empty() {
|
||||||
|
diagnostics.push((
|
||||||
|
file_id,
|
||||||
|
version,
|
||||||
|
references_to_diagnostics(references),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
diagnostics.push((file_id, version, Vec::new()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("Missing file contents for: {}", specifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
type TsDiagnostics = Vec<diagnostics::Diagnostic>;
|
||||||
|
|
||||||
|
fn get_diagnostic_message(diagnostic: &diagnostics::Diagnostic) -> String {
|
||||||
|
if let Some(message) = diagnostic.message_text.clone() {
|
||||||
|
message
|
||||||
|
} else if let Some(message_chain) = diagnostic.message_chain.clone() {
|
||||||
|
message_chain.format_message(0)
|
||||||
|
} else {
|
||||||
|
"[missing message]".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_lsp_related_information(
|
||||||
|
related_information: &Option<Vec<diagnostics::Diagnostic>>,
|
||||||
|
) -> Option<Vec<lsp_types::DiagnosticRelatedInformation>> {
|
||||||
|
if let Some(related) = related_information {
|
||||||
|
Some(
|
||||||
|
related
|
||||||
|
.iter()
|
||||||
|
.filter_map(|ri| {
|
||||||
|
if let (Some(source), Some(start), Some(end)) =
|
||||||
|
(&ri.source, &ri.start, &ri.end)
|
||||||
|
{
|
||||||
|
let uri = Url::parse(&source).unwrap();
|
||||||
|
Some(lsp_types::DiagnosticRelatedInformation {
|
||||||
|
location: lsp_types::Location {
|
||||||
|
uri,
|
||||||
|
range: to_lsp_range(start, end),
|
||||||
|
},
|
||||||
|
message: get_diagnostic_message(&ri),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ts_json_to_diagnostics(
|
||||||
|
value: Value,
|
||||||
|
) -> Result<Vec<lsp_types::Diagnostic>, AnyError> {
|
||||||
|
let ts_diagnostics: TsDiagnostics = serde_json::from_value(value)?;
|
||||||
|
Ok(
|
||||||
|
ts_diagnostics
|
||||||
|
.iter()
|
||||||
|
.filter_map(|d| {
|
||||||
|
if let (Some(start), Some(end)) = (&d.start, &d.end) {
|
||||||
|
Some(lsp_types::Diagnostic {
|
||||||
|
range: to_lsp_range(start, end),
|
||||||
|
severity: Some((&d.category).into()),
|
||||||
|
code: Some(lsp_types::NumberOrString::Number(d.code as i32)),
|
||||||
|
code_description: None,
|
||||||
|
source: Some("deno-ts".to_string()),
|
||||||
|
message: get_diagnostic_message(d),
|
||||||
|
related_information: to_lsp_related_information(
|
||||||
|
&d.related_information,
|
||||||
|
),
|
||||||
|
tags: match d.code {
|
||||||
|
// These are codes that indicate the variable is unused.
|
||||||
|
6133 | 6192 | 6196 => {
|
||||||
|
Some(vec![lsp_types::DiagnosticTag::Unnecessary])
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
data: None,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_ts_diagnostics(
|
||||||
|
state: &ServerStateSnapshot,
|
||||||
|
runtime: &mut JsRuntime,
|
||||||
|
) -> Result<DiagnosticVec, AnyError> {
|
||||||
|
if !state.config.settings.enable {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
let mut diagnostics = Vec::new();
|
||||||
|
let file_cache = state.file_cache.read().unwrap();
|
||||||
|
for (specifier, doc_data) in state.doc_data.iter() {
|
||||||
|
let file_id = file_cache.lookup(specifier).unwrap();
|
||||||
|
let version = doc_data.version;
|
||||||
|
let current_version = state.diagnostics.get_version(&file_id);
|
||||||
|
if version != current_version {
|
||||||
|
// TODO(@kitsonk): consider refactoring to get all diagnostics in one shot
|
||||||
|
// for a file.
|
||||||
|
let request_semantic_diagnostics =
|
||||||
|
tsc::RequestMethod::GetSemanticDiagnostics(specifier.clone());
|
||||||
|
let mut ts_diagnostics = ts_json_to_diagnostics(tsc::request(
|
||||||
|
runtime,
|
||||||
|
state,
|
||||||
|
request_semantic_diagnostics,
|
||||||
|
)?)?;
|
||||||
|
let request_suggestion_diagnostics =
|
||||||
|
tsc::RequestMethod::GetSuggestionDiagnostics(specifier.clone());
|
||||||
|
ts_diagnostics.append(&mut ts_json_to_diagnostics(tsc::request(
|
||||||
|
runtime,
|
||||||
|
state,
|
||||||
|
request_suggestion_diagnostics,
|
||||||
|
)?)?);
|
||||||
|
let request_syntactic_diagnostics =
|
||||||
|
tsc::RequestMethod::GetSyntacticDiagnostics(specifier.clone());
|
||||||
|
ts_diagnostics.append(&mut ts_json_to_diagnostics(tsc::request(
|
||||||
|
runtime,
|
||||||
|
state,
|
||||||
|
request_syntactic_diagnostics,
|
||||||
|
)?)?);
|
||||||
|
diagnostics.push((file_id, version, ts_diagnostics));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(diagnostics)
|
||||||
|
}
|
185
cli/lsp/dispatch.rs
Normal file
185
cli/lsp/dispatch.rs
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
use super::state::ServerState;
|
||||||
|
use super::state::ServerStateSnapshot;
|
||||||
|
use super::state::Task;
|
||||||
|
use super::utils::from_json;
|
||||||
|
use super::utils::is_canceled;
|
||||||
|
|
||||||
|
use deno_core::error::custom_error;
|
||||||
|
use deno_core::error::AnyError;
|
||||||
|
use lsp_server::ErrorCode;
|
||||||
|
use lsp_server::Notification;
|
||||||
|
use lsp_server::Request;
|
||||||
|
use lsp_server::RequestId;
|
||||||
|
use lsp_server::Response;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::fmt;
|
||||||
|
use std::panic;
|
||||||
|
|
||||||
|
pub struct NotificationDispatcher<'a> {
|
||||||
|
pub notification: Option<Notification>,
|
||||||
|
pub server_state: &'a mut ServerState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> NotificationDispatcher<'a> {
|
||||||
|
pub fn on<N>(
|
||||||
|
&mut self,
|
||||||
|
f: fn(&mut ServerState, N::Params) -> Result<(), AnyError>,
|
||||||
|
) -> Result<&mut Self, AnyError>
|
||||||
|
where
|
||||||
|
N: lsp_types::notification::Notification + 'static,
|
||||||
|
N::Params: DeserializeOwned + Send + 'static,
|
||||||
|
{
|
||||||
|
let notification = match self.notification.take() {
|
||||||
|
Some(it) => it,
|
||||||
|
None => return Ok(self),
|
||||||
|
};
|
||||||
|
let params = match notification.extract::<N::Params>(N::METHOD) {
|
||||||
|
Ok(it) => it,
|
||||||
|
Err(notification) => {
|
||||||
|
self.notification = Some(notification);
|
||||||
|
return Ok(self);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
f(self.server_state, params)?;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish(&mut self) {
|
||||||
|
if let Some(notification) = &self.notification {
|
||||||
|
if !notification.method.starts_with("$/") {
|
||||||
|
error!("unhandled notification: {:?}", notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn result_to_response<R>(
|
||||||
|
id: RequestId,
|
||||||
|
result: Result<R::Result, AnyError>,
|
||||||
|
) -> Response
|
||||||
|
where
|
||||||
|
R: lsp_types::request::Request + 'static,
|
||||||
|
R::Params: DeserializeOwned + 'static,
|
||||||
|
R::Result: Serialize + 'static,
|
||||||
|
{
|
||||||
|
match result {
|
||||||
|
Ok(response) => Response::new_ok(id, &response),
|
||||||
|
Err(err) => {
|
||||||
|
if is_canceled(&*err) {
|
||||||
|
Response::new_err(
|
||||||
|
id,
|
||||||
|
ErrorCode::ContentModified as i32,
|
||||||
|
"content modified".to_string(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Response::new_err(id, ErrorCode::InternalError as i32, err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RequestDispatcher<'a> {
|
||||||
|
pub request: Option<Request>,
|
||||||
|
pub server_state: &'a mut ServerState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> RequestDispatcher<'a> {
|
||||||
|
pub fn finish(&mut self) {
|
||||||
|
if let Some(request) = self.request.take() {
|
||||||
|
error!("unknown request: {:?}", request);
|
||||||
|
let response = Response::new_err(
|
||||||
|
request.id,
|
||||||
|
ErrorCode::MethodNotFound as i32,
|
||||||
|
"unknown request".to_string(),
|
||||||
|
);
|
||||||
|
self.server_state.respond(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle a request which will respond to the LSP client asynchronously via
|
||||||
|
/// a spawned thread.
|
||||||
|
pub fn on<R>(
|
||||||
|
&mut self,
|
||||||
|
f: fn(ServerStateSnapshot, R::Params) -> Result<R::Result, AnyError>,
|
||||||
|
) -> &mut Self
|
||||||
|
where
|
||||||
|
R: lsp_types::request::Request + 'static,
|
||||||
|
R::Params: DeserializeOwned + Send + fmt::Debug + 'static,
|
||||||
|
R::Result: Serialize + 'static,
|
||||||
|
{
|
||||||
|
let (id, params) = match self.parse::<R>() {
|
||||||
|
Some(it) => it,
|
||||||
|
None => return self,
|
||||||
|
};
|
||||||
|
self.server_state.spawn({
|
||||||
|
let state = self.server_state.snapshot();
|
||||||
|
move || {
|
||||||
|
let result = f(state, params);
|
||||||
|
Task::Response(result_to_response::<R>(id, result))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle a request which will respond synchronously, returning a result if
|
||||||
|
/// the request cannot be handled or has issues.
|
||||||
|
pub fn on_sync<R>(
|
||||||
|
&mut self,
|
||||||
|
f: fn(&mut ServerState, R::Params) -> Result<R::Result, AnyError>,
|
||||||
|
) -> Result<&mut Self, AnyError>
|
||||||
|
where
|
||||||
|
R: lsp_types::request::Request + 'static,
|
||||||
|
R::Params: DeserializeOwned + panic::UnwindSafe + fmt::Debug + 'static,
|
||||||
|
R::Result: Serialize + 'static,
|
||||||
|
{
|
||||||
|
let (id, params) = match self.parse::<R>() {
|
||||||
|
Some(it) => it,
|
||||||
|
None => return Ok(self),
|
||||||
|
};
|
||||||
|
let state = panic::AssertUnwindSafe(&mut *self.server_state);
|
||||||
|
|
||||||
|
let response = panic::catch_unwind(move || {
|
||||||
|
let result = f(state.0, params);
|
||||||
|
result_to_response::<R>(id, result)
|
||||||
|
})
|
||||||
|
.map_err(|_err| {
|
||||||
|
custom_error(
|
||||||
|
"SyncTaskPanic",
|
||||||
|
format!("sync task {:?} panicked", R::METHOD),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
self.server_state.respond(response);
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse<R>(&mut self) -> Option<(RequestId, R::Params)>
|
||||||
|
where
|
||||||
|
R: lsp_types::request::Request + 'static,
|
||||||
|
R::Params: DeserializeOwned + 'static,
|
||||||
|
{
|
||||||
|
let request = match &self.request {
|
||||||
|
Some(request) if request.method == R::METHOD => {
|
||||||
|
self.request.take().unwrap()
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = from_json(R::METHOD, request.params);
|
||||||
|
match response {
|
||||||
|
Ok(params) => Some((request.id, params)),
|
||||||
|
Err(err) => {
|
||||||
|
let response = Response::new_err(
|
||||||
|
request.id,
|
||||||
|
ErrorCode::InvalidParams as i32,
|
||||||
|
err.to_string(),
|
||||||
|
);
|
||||||
|
self.server_state.respond(response);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
266
cli/lsp/handlers.rs
Normal file
266
cli/lsp/handlers.rs
Normal file
|
@ -0,0 +1,266 @@
|
||||||
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
use super::lsp_extensions;
|
||||||
|
use super::state::ServerState;
|
||||||
|
use super::state::ServerStateSnapshot;
|
||||||
|
use super::text;
|
||||||
|
use super::tsc;
|
||||||
|
use super::utils;
|
||||||
|
|
||||||
|
use deno_core::error::custom_error;
|
||||||
|
use deno_core::error::AnyError;
|
||||||
|
use deno_core::serde_json;
|
||||||
|
use deno_core::ModuleSpecifier;
|
||||||
|
use dprint_plugin_typescript as dprint;
|
||||||
|
use lsp_types::DocumentFormattingParams;
|
||||||
|
use lsp_types::DocumentHighlight;
|
||||||
|
use lsp_types::DocumentHighlightParams;
|
||||||
|
use lsp_types::GotoDefinitionParams;
|
||||||
|
use lsp_types::GotoDefinitionResponse;
|
||||||
|
use lsp_types::Hover;
|
||||||
|
use lsp_types::HoverParams;
|
||||||
|
use lsp_types::Location;
|
||||||
|
use lsp_types::ReferenceParams;
|
||||||
|
use lsp_types::TextEdit;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn get_line_index(
|
||||||
|
state: &mut ServerState,
|
||||||
|
specifier: &ModuleSpecifier,
|
||||||
|
) -> Result<Vec<u32>, AnyError> {
|
||||||
|
let line_index = if specifier.as_url().scheme() == "asset" {
|
||||||
|
if let Some(source) = tsc::get_asset(specifier.as_url().path()) {
|
||||||
|
text::index_lines(source)
|
||||||
|
} else {
|
||||||
|
return Err(custom_error(
|
||||||
|
"NotFound",
|
||||||
|
format!("asset source missing: {}", specifier),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let file_cache = state.file_cache.read().unwrap();
|
||||||
|
if let Some(file_id) = file_cache.lookup(specifier) {
|
||||||
|
let file_text = file_cache.get_contents(file_id)?;
|
||||||
|
text::index_lines(&file_text)
|
||||||
|
} else {
|
||||||
|
let mut sources = state.sources.write().unwrap();
|
||||||
|
if let Some(line_index) = sources.get_line_index(specifier) {
|
||||||
|
line_index
|
||||||
|
} else {
|
||||||
|
return Err(custom_error(
|
||||||
|
"NotFound",
|
||||||
|
format!("source for specifier not found: {}", specifier),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(line_index)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_formatting(
|
||||||
|
state: ServerStateSnapshot,
|
||||||
|
params: DocumentFormattingParams,
|
||||||
|
) -> Result<Option<Vec<TextEdit>>, AnyError> {
|
||||||
|
let specifier = utils::normalize_url(params.text_document.uri.clone());
|
||||||
|
let file_cache = state.file_cache.read().unwrap();
|
||||||
|
let file_id = file_cache.lookup(&specifier).unwrap();
|
||||||
|
let file_text = file_cache.get_contents(file_id)?;
|
||||||
|
|
||||||
|
let file_path = if let Ok(file_path) = params.text_document.uri.to_file_path()
|
||||||
|
{
|
||||||
|
file_path
|
||||||
|
} else {
|
||||||
|
PathBuf::from(params.text_document.uri.path())
|
||||||
|
};
|
||||||
|
let config = dprint::configuration::ConfigurationBuilder::new()
|
||||||
|
.deno()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// TODO(@kitsonk) this could be handled better in `cli/tools/fmt.rs` in the
|
||||||
|
// future.
|
||||||
|
let new_text = dprint::format_text(&file_path, &file_text, &config)
|
||||||
|
.map_err(|e| custom_error("FormatError", e))?;
|
||||||
|
|
||||||
|
let text_edits = text::get_edits(&file_text, &new_text);
|
||||||
|
if text_edits.is_empty() {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
Ok(Some(text_edits))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_document_highlight(
|
||||||
|
state: &mut ServerState,
|
||||||
|
params: DocumentHighlightParams,
|
||||||
|
) -> Result<Option<Vec<DocumentHighlight>>, AnyError> {
|
||||||
|
let specifier = utils::normalize_url(
|
||||||
|
params.text_document_position_params.text_document.uri,
|
||||||
|
);
|
||||||
|
let line_index = get_line_index(state, &specifier)?;
|
||||||
|
let server_state = state.snapshot();
|
||||||
|
let files_to_search = vec![specifier.clone()];
|
||||||
|
let maybe_document_highlights: Option<Vec<tsc::DocumentHighlights>> =
|
||||||
|
serde_json::from_value(tsc::request(
|
||||||
|
&mut state.ts_runtime,
|
||||||
|
&server_state,
|
||||||
|
tsc::RequestMethod::GetDocumentHighlights((
|
||||||
|
specifier,
|
||||||
|
text::to_char_pos(
|
||||||
|
&line_index,
|
||||||
|
params.text_document_position_params.position,
|
||||||
|
),
|
||||||
|
files_to_search,
|
||||||
|
)),
|
||||||
|
)?)?;
|
||||||
|
|
||||||
|
if let Some(document_highlights) = maybe_document_highlights {
|
||||||
|
Ok(Some(
|
||||||
|
document_highlights
|
||||||
|
.into_iter()
|
||||||
|
.map(|dh| dh.to_highlight(&line_index))
|
||||||
|
.flatten()
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_goto_definition(
|
||||||
|
state: &mut ServerState,
|
||||||
|
params: GotoDefinitionParams,
|
||||||
|
) -> Result<Option<GotoDefinitionResponse>, AnyError> {
|
||||||
|
let specifier = utils::normalize_url(
|
||||||
|
params.text_document_position_params.text_document.uri,
|
||||||
|
);
|
||||||
|
let line_index = get_line_index(state, &specifier)?;
|
||||||
|
let server_state = state.snapshot();
|
||||||
|
let maybe_definition: Option<tsc::DefinitionInfoAndBoundSpan> =
|
||||||
|
serde_json::from_value(tsc::request(
|
||||||
|
&mut state.ts_runtime,
|
||||||
|
&server_state,
|
||||||
|
tsc::RequestMethod::GetDefinition((
|
||||||
|
specifier,
|
||||||
|
text::to_char_pos(
|
||||||
|
&line_index,
|
||||||
|
params.text_document_position_params.position,
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
)?)?;
|
||||||
|
|
||||||
|
if let Some(definition) = maybe_definition {
|
||||||
|
Ok(
|
||||||
|
definition
|
||||||
|
.to_definition(&line_index, |s| get_line_index(state, &s).unwrap()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_hover(
|
||||||
|
state: &mut ServerState,
|
||||||
|
params: HoverParams,
|
||||||
|
) -> Result<Option<Hover>, AnyError> {
|
||||||
|
let specifier = utils::normalize_url(
|
||||||
|
params.text_document_position_params.text_document.uri,
|
||||||
|
);
|
||||||
|
let line_index = get_line_index(state, &specifier)?;
|
||||||
|
let server_state = state.snapshot();
|
||||||
|
let maybe_quick_info: Option<tsc::QuickInfo> =
|
||||||
|
serde_json::from_value(tsc::request(
|
||||||
|
&mut state.ts_runtime,
|
||||||
|
&server_state,
|
||||||
|
tsc::RequestMethod::GetQuickInfo((
|
||||||
|
specifier,
|
||||||
|
text::to_char_pos(
|
||||||
|
&line_index,
|
||||||
|
params.text_document_position_params.position,
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
)?)?;
|
||||||
|
|
||||||
|
if let Some(quick_info) = maybe_quick_info {
|
||||||
|
Ok(Some(quick_info.to_hover(&line_index)))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_references(
|
||||||
|
state: &mut ServerState,
|
||||||
|
params: ReferenceParams,
|
||||||
|
) -> Result<Option<Vec<Location>>, AnyError> {
|
||||||
|
let specifier =
|
||||||
|
utils::normalize_url(params.text_document_position.text_document.uri);
|
||||||
|
let line_index = get_line_index(state, &specifier)?;
|
||||||
|
let server_state = state.snapshot();
|
||||||
|
let maybe_references: Option<Vec<tsc::ReferenceEntry>> =
|
||||||
|
serde_json::from_value(tsc::request(
|
||||||
|
&mut state.ts_runtime,
|
||||||
|
&server_state,
|
||||||
|
tsc::RequestMethod::GetReferences((
|
||||||
|
specifier,
|
||||||
|
text::to_char_pos(&line_index, params.text_document_position.position),
|
||||||
|
)),
|
||||||
|
)?)?;
|
||||||
|
|
||||||
|
if let Some(references) = maybe_references {
|
||||||
|
let mut results = Vec::new();
|
||||||
|
for reference in references {
|
||||||
|
if !params.context.include_declaration && reference.is_definition {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let reference_specifier =
|
||||||
|
ModuleSpecifier::resolve_url(&reference.file_name).unwrap();
|
||||||
|
let line_index = get_line_index(state, &reference_specifier)?;
|
||||||
|
results.push(reference.to_location(&line_index));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(results))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_virtual_text_document(
|
||||||
|
state: ServerStateSnapshot,
|
||||||
|
params: lsp_extensions::VirtualTextDocumentParams,
|
||||||
|
) -> Result<String, AnyError> {
|
||||||
|
let specifier = utils::normalize_url(params.text_document.uri);
|
||||||
|
let url = specifier.as_url();
|
||||||
|
let contents = if url.as_str() == "deno:///status.md" {
|
||||||
|
let file_cache = state.file_cache.read().unwrap();
|
||||||
|
format!(
|
||||||
|
r#"# Deno Language Server Status
|
||||||
|
|
||||||
|
- Documents in memory: {}
|
||||||
|
|
||||||
|
"#,
|
||||||
|
file_cache.len()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
match url.scheme() {
|
||||||
|
"asset" => {
|
||||||
|
if let Some(text) = tsc::get_asset(url.path()) {
|
||||||
|
text.to_string()
|
||||||
|
} else {
|
||||||
|
error!("Missing asset: {}", specifier);
|
||||||
|
"".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let mut sources = state.sources.write().unwrap();
|
||||||
|
if let Some(text) = sources.get_text(&specifier) {
|
||||||
|
text
|
||||||
|
} else {
|
||||||
|
return Err(custom_error(
|
||||||
|
"NotFound",
|
||||||
|
format!("The cached sources was not found: {}", specifier),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(contents)
|
||||||
|
}
|
26
cli/lsp/lsp_extensions.rs
Normal file
26
cli/lsp/lsp_extensions.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
///!
|
||||||
|
///! Extensions to the language service protocol that are specific to Deno.
|
||||||
|
///!
|
||||||
|
use deno_core::serde::Deserialize;
|
||||||
|
use deno_core::serde::Serialize;
|
||||||
|
use lsp_types::request::Request;
|
||||||
|
use lsp_types::TextDocumentIdentifier;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct VirtualTextDocumentParams {
|
||||||
|
pub text_document: TextDocumentIdentifier,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request a _virtual_ text document from the server. Used for example to
|
||||||
|
/// provide a status document of the language server which can be viewed in the
|
||||||
|
/// IDE.
|
||||||
|
pub enum VirtualTextDocument {}
|
||||||
|
|
||||||
|
impl Request for VirtualTextDocument {
|
||||||
|
type Params = VirtualTextDocumentParams;
|
||||||
|
type Result = String;
|
||||||
|
const METHOD: &'static str = "deno/virtualTextDocument";
|
||||||
|
}
|
126
cli/lsp/memory_cache.rs
Normal file
126
cli/lsp/memory_cache.rs
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
use deno_core::error::AnyError;
|
||||||
|
use deno_core::ModuleSpecifier;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
|
||||||
|
pub struct FileId(pub u32);
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, Copy, Clone, Debug)]
|
||||||
|
pub enum ChangeKind {
|
||||||
|
Create,
|
||||||
|
Modify,
|
||||||
|
Delete,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ChangedFile {
|
||||||
|
pub change_kind: ChangeKind,
|
||||||
|
pub file_id: FileId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct SpecifierInterner {
|
||||||
|
map: HashMap<ModuleSpecifier, FileId>,
|
||||||
|
vec: Vec<ModuleSpecifier>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpecifierInterner {
|
||||||
|
pub fn get(&self, specifier: &ModuleSpecifier) -> Option<FileId> {
|
||||||
|
self.map.get(specifier).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn intern(&mut self, specifier: ModuleSpecifier) -> FileId {
|
||||||
|
if let Some(id) = self.get(&specifier) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
let id = FileId(self.vec.len() as u32);
|
||||||
|
self.map.insert(specifier.clone(), id);
|
||||||
|
self.vec.push(specifier);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lookup(&self, id: FileId) -> &ModuleSpecifier {
|
||||||
|
&self.vec[id.0 as usize]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct MemoryCache {
|
||||||
|
data: Vec<Option<Vec<u8>>>,
|
||||||
|
interner: SpecifierInterner,
|
||||||
|
changes: Vec<ChangedFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemoryCache {
|
||||||
|
fn alloc_file_id(&mut self, specifier: ModuleSpecifier) -> FileId {
|
||||||
|
let file_id = self.interner.intern(specifier);
|
||||||
|
let idx = file_id.0 as usize;
|
||||||
|
let len = self.data.len().max(idx + 1);
|
||||||
|
self.data.resize_with(len, || None);
|
||||||
|
file_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, file_id: FileId) -> &Option<Vec<u8>> {
|
||||||
|
&self.data[file_id.0 as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_contents(&self, file_id: FileId) -> Result<String, AnyError> {
|
||||||
|
String::from_utf8(self.get(file_id).as_deref().unwrap().to_vec())
|
||||||
|
.map_err(|err| err.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_mut(&mut self, file_id: FileId) -> &mut Option<Vec<u8>> {
|
||||||
|
&mut self.data[file_id.0 as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_specifier(&self, file_id: FileId) -> &ModuleSpecifier {
|
||||||
|
self.interner.lookup(file_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.data.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lookup(&self, specifier: &ModuleSpecifier) -> Option<FileId> {
|
||||||
|
self
|
||||||
|
.interner
|
||||||
|
.get(specifier)
|
||||||
|
.filter(|&it| self.get(it).is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_contents(
|
||||||
|
&mut self,
|
||||||
|
specifier: ModuleSpecifier,
|
||||||
|
contents: Option<Vec<u8>>,
|
||||||
|
) {
|
||||||
|
let file_id = self.alloc_file_id(specifier);
|
||||||
|
let change_kind = match (self.get(file_id), &contents) {
|
||||||
|
(None, None) => return,
|
||||||
|
(None, Some(_)) => ChangeKind::Create,
|
||||||
|
(Some(_), None) => ChangeKind::Delete,
|
||||||
|
(Some(old), Some(new)) if old == new => return,
|
||||||
|
(Some(_), Some(_)) => ChangeKind::Modify,
|
||||||
|
};
|
||||||
|
|
||||||
|
*self.get_mut(file_id) = contents;
|
||||||
|
self.changes.push(ChangedFile {
|
||||||
|
file_id,
|
||||||
|
change_kind,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_changes(&mut self) -> Vec<ChangedFile> {
|
||||||
|
mem::take(&mut self.changes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for MemoryCache {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.debug_struct("MemoryCache")
|
||||||
|
.field("no_files", &self.data.len())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
415
cli/lsp/mod.rs
Normal file
415
cli/lsp/mod.rs
Normal file
|
@ -0,0 +1,415 @@
|
||||||
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
mod analysis;
|
||||||
|
mod capabilities;
|
||||||
|
mod config;
|
||||||
|
mod diagnostics;
|
||||||
|
mod dispatch;
|
||||||
|
mod handlers;
|
||||||
|
mod lsp_extensions;
|
||||||
|
mod memory_cache;
|
||||||
|
mod sources;
|
||||||
|
mod state;
|
||||||
|
mod text;
|
||||||
|
mod tsc;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
use config::Config;
|
||||||
|
use diagnostics::DiagnosticSource;
|
||||||
|
use dispatch::NotificationDispatcher;
|
||||||
|
use dispatch::RequestDispatcher;
|
||||||
|
use state::DocumentData;
|
||||||
|
use state::Event;
|
||||||
|
use state::ServerState;
|
||||||
|
use state::Status;
|
||||||
|
use state::Task;
|
||||||
|
use text::apply_content_changes;
|
||||||
|
|
||||||
|
use crate::tsc_config::TsConfig;
|
||||||
|
|
||||||
|
use crossbeam_channel::Receiver;
|
||||||
|
use deno_core::error::custom_error;
|
||||||
|
use deno_core::error::AnyError;
|
||||||
|
use deno_core::serde_json;
|
||||||
|
use deno_core::serde_json::json;
|
||||||
|
use lsp_server::Connection;
|
||||||
|
use lsp_server::ErrorCode;
|
||||||
|
use lsp_server::Message;
|
||||||
|
use lsp_server::Notification;
|
||||||
|
use lsp_server::Request;
|
||||||
|
use lsp_server::RequestId;
|
||||||
|
use lsp_server::Response;
|
||||||
|
use lsp_types::notification::Notification as _;
|
||||||
|
use lsp_types::Diagnostic;
|
||||||
|
use lsp_types::InitializeParams;
|
||||||
|
use lsp_types::InitializeResult;
|
||||||
|
use lsp_types::ServerInfo;
|
||||||
|
use std::env;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
pub fn start() -> Result<(), AnyError> {
|
||||||
|
info!("Starting Deno language server...");
|
||||||
|
|
||||||
|
let (connection, io_threads) = Connection::stdio();
|
||||||
|
let (initialize_id, initialize_params) = connection.initialize_start()?;
|
||||||
|
let initialize_params: InitializeParams =
|
||||||
|
serde_json::from_value(initialize_params)?;
|
||||||
|
|
||||||
|
let capabilities =
|
||||||
|
capabilities::server_capabilities(&initialize_params.capabilities);
|
||||||
|
|
||||||
|
let version = format!(
|
||||||
|
"{} ({}, {})",
|
||||||
|
crate::version::deno(),
|
||||||
|
env!("PROFILE"),
|
||||||
|
env!("TARGET")
|
||||||
|
);
|
||||||
|
|
||||||
|
info!(" version: {}", version);
|
||||||
|
|
||||||
|
let initialize_result = InitializeResult {
|
||||||
|
capabilities,
|
||||||
|
server_info: Some(ServerInfo {
|
||||||
|
name: "deno-language-server".to_string(),
|
||||||
|
version: Some(version),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let initialize_result = serde_json::to_value(initialize_result)?;
|
||||||
|
|
||||||
|
connection.initialize_finish(initialize_id, initialize_result)?;
|
||||||
|
|
||||||
|
if let Some(client_info) = initialize_params.client_info {
|
||||||
|
info!(
|
||||||
|
"Connected to \"{}\" {}",
|
||||||
|
client_info.name,
|
||||||
|
client_info.version.unwrap_or_default()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut config = Config::default();
|
||||||
|
if let Some(value) = initialize_params.initialization_options {
|
||||||
|
config.update(value)?;
|
||||||
|
}
|
||||||
|
config.update_capabilities(&initialize_params.capabilities);
|
||||||
|
|
||||||
|
let mut server_state = state::ServerState::new(connection.sender, config);
|
||||||
|
let state = server_state.snapshot();
|
||||||
|
|
||||||
|
// TODO(@kitsonk) need to make this configurable, respect unstable
|
||||||
|
let ts_config = TsConfig::new(json!({
|
||||||
|
"allowJs": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"lib": ["deno.ns", "deno.window"],
|
||||||
|
"module": "esnext",
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"target": "esnext",
|
||||||
|
}));
|
||||||
|
tsc::request(
|
||||||
|
&mut server_state.ts_runtime,
|
||||||
|
&state,
|
||||||
|
tsc::RequestMethod::Configure(ts_config),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// listen for events and run the main loop
|
||||||
|
server_state.run(connection.receiver)?;
|
||||||
|
|
||||||
|
io_threads.join()?;
|
||||||
|
info!("Stop language server");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerState {
|
||||||
|
fn handle_event(&mut self, event: Event) -> Result<(), AnyError> {
|
||||||
|
let received = Instant::now();
|
||||||
|
debug!("handle_event({:?})", event);
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Event::Message(message) => match message {
|
||||||
|
Message::Request(request) => self.on_request(request, received)?,
|
||||||
|
Message::Notification(notification) => {
|
||||||
|
self.on_notification(notification)?
|
||||||
|
}
|
||||||
|
Message::Response(response) => self.complete_request(response),
|
||||||
|
},
|
||||||
|
Event::Task(mut task) => loop {
|
||||||
|
match task {
|
||||||
|
Task::Response(response) => self.respond(response),
|
||||||
|
Task::Diagnostics((source, diagnostics_per_file)) => {
|
||||||
|
for (file_id, version, diagnostics) in diagnostics_per_file {
|
||||||
|
self.diagnostics.set(
|
||||||
|
file_id,
|
||||||
|
source.clone(),
|
||||||
|
version,
|
||||||
|
diagnostics,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task = match self.task_receiver.try_recv() {
|
||||||
|
Ok(task) => task,
|
||||||
|
Err(_) => break,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// process server sent notifications, like diagnostics
|
||||||
|
// TODO(@kitsonk) currently all of these refresh all open documents, though
|
||||||
|
// in a lot of cases, like linting, we would only care about the files
|
||||||
|
// themselves that have changed
|
||||||
|
if self.process_changes() {
|
||||||
|
debug!("process changes");
|
||||||
|
let state = self.snapshot();
|
||||||
|
self.spawn(move || {
|
||||||
|
let diagnostics = diagnostics::generate_linting_diagnostics(&state);
|
||||||
|
Task::Diagnostics((DiagnosticSource::Lint, diagnostics))
|
||||||
|
});
|
||||||
|
// TODO(@kitsonk) isolates do not have Send to be safely sent between
|
||||||
|
// threads, so I am not sure this is the best way to handle queuing up of
|
||||||
|
// getting the diagnostics from the isolate.
|
||||||
|
let state = self.snapshot();
|
||||||
|
let diagnostics =
|
||||||
|
diagnostics::generate_ts_diagnostics(&state, &mut self.ts_runtime)?;
|
||||||
|
self.spawn(move || {
|
||||||
|
Task::Diagnostics((DiagnosticSource::TypeScript, diagnostics))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// process any changes to the diagnostics
|
||||||
|
if let Some(diagnostic_changes) = self.diagnostics.take_changes() {
|
||||||
|
debug!("diagnostics have changed");
|
||||||
|
let state = self.snapshot();
|
||||||
|
for file_id in diagnostic_changes {
|
||||||
|
let file_cache = state.file_cache.read().unwrap();
|
||||||
|
// TODO(@kitsonk) not totally happy with the way we collect and store
|
||||||
|
// different types of diagnostics and offer them up to the client, we
|
||||||
|
// do need to send "empty" vectors though when a particular feature is
|
||||||
|
// disabled, otherwise the client will not clear down previous
|
||||||
|
// diagnostics
|
||||||
|
let mut diagnostics: Vec<Diagnostic> = if state.config.settings.lint {
|
||||||
|
self
|
||||||
|
.diagnostics
|
||||||
|
.diagnostics_for(file_id, DiagnosticSource::Lint)
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
if state.config.settings.enable {
|
||||||
|
diagnostics.extend(
|
||||||
|
self
|
||||||
|
.diagnostics
|
||||||
|
.diagnostics_for(file_id, DiagnosticSource::TypeScript)
|
||||||
|
.cloned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let specifier = file_cache.get_specifier(file_id);
|
||||||
|
let uri = specifier.as_url().clone();
|
||||||
|
let version = if let Some(doc_data) = self.doc_data.get(specifier) {
|
||||||
|
doc_data.version
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
self.send_notification::<lsp_types::notification::PublishDiagnostics>(
|
||||||
|
lsp_types::PublishDiagnosticsParams {
|
||||||
|
uri,
|
||||||
|
diagnostics,
|
||||||
|
version,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_notification(
|
||||||
|
&mut self,
|
||||||
|
notification: Notification,
|
||||||
|
) -> Result<(), AnyError> {
|
||||||
|
NotificationDispatcher {
|
||||||
|
notification: Some(notification),
|
||||||
|
server_state: self,
|
||||||
|
}
|
||||||
|
// TODO(@kitsonk) this is just stubbed out and we don't currently actually
|
||||||
|
// cancel in progress work, though most of our work isn't long running
|
||||||
|
.on::<lsp_types::notification::Cancel>(|state, params| {
|
||||||
|
let id: RequestId = match params.id {
|
||||||
|
lsp_types::NumberOrString::Number(id) => id.into(),
|
||||||
|
lsp_types::NumberOrString::String(id) => id.into(),
|
||||||
|
};
|
||||||
|
state.cancel(id);
|
||||||
|
Ok(())
|
||||||
|
})?
|
||||||
|
.on::<lsp_types::notification::DidOpenTextDocument>(|state, params| {
|
||||||
|
if params.text_document.uri.scheme() == "deno" {
|
||||||
|
// we can ignore virtual text documents opening, as they don't need to
|
||||||
|
// be tracked in memory, as they are static assets that won't change
|
||||||
|
// already managed by the language service
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let specifier = utils::normalize_url(params.text_document.uri);
|
||||||
|
if state
|
||||||
|
.doc_data
|
||||||
|
.insert(
|
||||||
|
specifier.clone(),
|
||||||
|
DocumentData::new(
|
||||||
|
specifier.clone(),
|
||||||
|
params.text_document.version,
|
||||||
|
¶ms.text_document.text,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
error!("duplicate DidOpenTextDocument: {}", specifier);
|
||||||
|
}
|
||||||
|
state
|
||||||
|
.file_cache
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.set_contents(specifier, Some(params.text_document.text.into_bytes()));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?
|
||||||
|
.on::<lsp_types::notification::DidChangeTextDocument>(|state, params| {
|
||||||
|
let specifier = utils::normalize_url(params.text_document.uri);
|
||||||
|
let mut file_cache = state.file_cache.write().unwrap();
|
||||||
|
let file_id = file_cache.lookup(&specifier).unwrap();
|
||||||
|
let mut content = file_cache.get_contents(file_id)?;
|
||||||
|
apply_content_changes(&mut content, params.content_changes);
|
||||||
|
let doc_data = state.doc_data.get_mut(&specifier).unwrap();
|
||||||
|
doc_data.update(params.text_document.version, &content, None);
|
||||||
|
file_cache.set_contents(specifier, Some(content.into_bytes()));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?
|
||||||
|
.on::<lsp_types::notification::DidCloseTextDocument>(|state, params| {
|
||||||
|
if params.text_document.uri.scheme() == "deno" {
|
||||||
|
// we can ignore virtual text documents opening, as they don't need to
|
||||||
|
// be tracked in memory, as they are static assets that won't change
|
||||||
|
// already managed by the language service
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let specifier = utils::normalize_url(params.text_document.uri);
|
||||||
|
if state.doc_data.remove(&specifier).is_none() {
|
||||||
|
error!("orphaned document: {}", specifier);
|
||||||
|
}
|
||||||
|
// TODO(@kitsonk) should we do garbage collection on the diagnostics?
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?
|
||||||
|
.on::<lsp_types::notification::DidSaveTextDocument>(|_state, _params| {
|
||||||
|
// nothing to do yet... cleanup things?
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?
|
||||||
|
.on::<lsp_types::notification::DidChangeConfiguration>(|state, _params| {
|
||||||
|
state.send_request::<lsp_types::request::WorkspaceConfiguration>(
|
||||||
|
lsp_types::ConfigurationParams {
|
||||||
|
items: vec![lsp_types::ConfigurationItem {
|
||||||
|
scope_uri: None,
|
||||||
|
section: Some("deno".to_string()),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
|state, response| {
|
||||||
|
let Response { error, result, .. } = response;
|
||||||
|
|
||||||
|
match (error, result) {
|
||||||
|
(Some(err), _) => {
|
||||||
|
error!("failed to fetch the extension settings: {:?}", err);
|
||||||
|
}
|
||||||
|
(None, Some(config)) => {
|
||||||
|
if let Some(config) = config.get(0) {
|
||||||
|
if let Err(err) = state.config.update(config.clone()) {
|
||||||
|
error!("failed to update settings: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(None, None) => {
|
||||||
|
error!("received empty extension settings from the client");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?
|
||||||
|
.finish();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_request(
|
||||||
|
&mut self,
|
||||||
|
request: Request,
|
||||||
|
received: Instant,
|
||||||
|
) -> Result<(), AnyError> {
|
||||||
|
self.register_request(&request, received);
|
||||||
|
|
||||||
|
if self.shutdown_requested {
|
||||||
|
self.respond(Response::new_err(
|
||||||
|
request.id,
|
||||||
|
ErrorCode::InvalidRequest as i32,
|
||||||
|
"Shutdown already requested".to_string(),
|
||||||
|
));
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.status == Status::Loading && request.method != "shutdown" {
|
||||||
|
self.respond(Response::new_err(
|
||||||
|
request.id,
|
||||||
|
ErrorCode::ContentModified as i32,
|
||||||
|
"Deno Language Server is still loading...".to_string(),
|
||||||
|
));
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestDispatcher {
|
||||||
|
request: Some(request),
|
||||||
|
server_state: self,
|
||||||
|
}
|
||||||
|
.on_sync::<lsp_types::request::Shutdown>(|s, ()| {
|
||||||
|
s.shutdown_requested = true;
|
||||||
|
Ok(())
|
||||||
|
})?
|
||||||
|
.on_sync::<lsp_types::request::DocumentHighlightRequest>(
|
||||||
|
handlers::handle_document_highlight,
|
||||||
|
)?
|
||||||
|
.on_sync::<lsp_types::request::GotoDefinition>(
|
||||||
|
handlers::handle_goto_definition,
|
||||||
|
)?
|
||||||
|
.on_sync::<lsp_types::request::HoverRequest>(handlers::handle_hover)?
|
||||||
|
.on_sync::<lsp_types::request::References>(handlers::handle_references)?
|
||||||
|
.on::<lsp_types::request::Formatting>(handlers::handle_formatting)
|
||||||
|
.on::<lsp_extensions::VirtualTextDocument>(
|
||||||
|
handlers::handle_virtual_text_document,
|
||||||
|
)
|
||||||
|
.finish();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start consuming events from the provided receiver channel.
|
||||||
|
pub fn run(mut self, inbox: Receiver<Message>) -> Result<(), AnyError> {
|
||||||
|
// currently we don't need to do any other loading or tasks, so as soon as
|
||||||
|
// we run we are "ready"
|
||||||
|
self.transition(Status::Ready);
|
||||||
|
|
||||||
|
while let Some(event) = self.next_event(&inbox) {
|
||||||
|
if let Event::Message(Message::Notification(notification)) = &event {
|
||||||
|
if notification.method == lsp_types::notification::Exit::METHOD {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.handle_event(event)?
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(custom_error(
|
||||||
|
"ClientError",
|
||||||
|
"Client exited without proper shutdown sequence.",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
372
cli/lsp/sources.rs
Normal file
372
cli/lsp/sources.rs
Normal file
|
@ -0,0 +1,372 @@
|
||||||
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
use super::analysis;
|
||||||
|
use super::text;
|
||||||
|
|
||||||
|
use crate::file_fetcher::get_source_from_bytes;
|
||||||
|
use crate::file_fetcher::map_content_type;
|
||||||
|
use crate::http_cache;
|
||||||
|
use crate::http_cache::HttpCache;
|
||||||
|
use crate::media_type::MediaType;
|
||||||
|
use crate::text_encoding;
|
||||||
|
|
||||||
|
use deno_core::serde_json;
|
||||||
|
use deno_core::ModuleSpecifier;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
struct Metadata {
|
||||||
|
dependencies: Option<HashMap<String, analysis::Dependency>>,
|
||||||
|
maybe_types: Option<analysis::ResolvedImport>,
|
||||||
|
media_type: MediaType,
|
||||||
|
source: String,
|
||||||
|
version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct Sources {
|
||||||
|
http_cache: HttpCache,
|
||||||
|
metadata: HashMap<ModuleSpecifier, Metadata>,
|
||||||
|
redirects: HashMap<ModuleSpecifier, ModuleSpecifier>,
|
||||||
|
remotes: HashMap<ModuleSpecifier, PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sources {
|
||||||
|
pub fn new(location: &Path) -> Self {
|
||||||
|
Self {
|
||||||
|
http_cache: HttpCache::new(location),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains(&mut self, specifier: &ModuleSpecifier) -> bool {
|
||||||
|
if let Some(specifier) = self.resolve_specifier(specifier) {
|
||||||
|
if self.get_metadata(&specifier).is_some() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_length(&mut self, specifier: &ModuleSpecifier) -> Option<usize> {
|
||||||
|
let specifier = self.resolve_specifier(specifier)?;
|
||||||
|
let metadata = self.get_metadata(&specifier)?;
|
||||||
|
Some(metadata.source.chars().count())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_line_index(
|
||||||
|
&mut self,
|
||||||
|
specifier: &ModuleSpecifier,
|
||||||
|
) -> Option<Vec<u32>> {
|
||||||
|
let specifier = self.resolve_specifier(specifier)?;
|
||||||
|
let metadata = self.get_metadata(&specifier)?;
|
||||||
|
Some(text::index_lines(&metadata.source))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_media_type(
|
||||||
|
&mut self,
|
||||||
|
specifier: &ModuleSpecifier,
|
||||||
|
) -> Option<MediaType> {
|
||||||
|
let specifier = self.resolve_specifier(specifier)?;
|
||||||
|
let metadata = self.get_metadata(&specifier)?;
|
||||||
|
Some(metadata.media_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_metadata(&mut self, specifier: &ModuleSpecifier) -> Option<Metadata> {
|
||||||
|
if let Some(metadata) = self.metadata.get(specifier).cloned() {
|
||||||
|
if let Some(current_version) = self.get_script_version(specifier) {
|
||||||
|
if metadata.version == current_version {
|
||||||
|
return Some(metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let version = self.get_script_version(specifier)?;
|
||||||
|
let path = self.get_path(specifier)?;
|
||||||
|
if let Ok(bytes) = fs::read(path) {
|
||||||
|
if specifier.as_url().scheme() == "file" {
|
||||||
|
let charset = text_encoding::detect_charset(&bytes).to_string();
|
||||||
|
if let Ok(source) = get_source_from_bytes(bytes, Some(charset)) {
|
||||||
|
let media_type = MediaType::from(specifier);
|
||||||
|
let mut maybe_types = None;
|
||||||
|
let dependencies = if let Some((dependencies, mt)) =
|
||||||
|
analysis::analyze_dependencies(
|
||||||
|
&specifier,
|
||||||
|
&source,
|
||||||
|
&media_type,
|
||||||
|
None,
|
||||||
|
) {
|
||||||
|
maybe_types = mt;
|
||||||
|
Some(dependencies)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let metadata = Metadata {
|
||||||
|
dependencies,
|
||||||
|
maybe_types,
|
||||||
|
media_type,
|
||||||
|
source,
|
||||||
|
version,
|
||||||
|
};
|
||||||
|
self.metadata.insert(specifier.clone(), metadata.clone());
|
||||||
|
Some(metadata)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let headers = self.get_remote_headers(specifier)?;
|
||||||
|
let maybe_content_type = headers.get("content-type").cloned();
|
||||||
|
let (media_type, maybe_charset) =
|
||||||
|
map_content_type(specifier, maybe_content_type);
|
||||||
|
if let Ok(source) = get_source_from_bytes(bytes, maybe_charset) {
|
||||||
|
let mut maybe_types =
|
||||||
|
if let Some(types) = headers.get("x-typescript-types") {
|
||||||
|
Some(analysis::resolve_import(types, &specifier, None))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let dependencies = if let Some((dependencies, mt)) =
|
||||||
|
analysis::analyze_dependencies(
|
||||||
|
&specifier,
|
||||||
|
&source,
|
||||||
|
&media_type,
|
||||||
|
None,
|
||||||
|
) {
|
||||||
|
if maybe_types.is_none() {
|
||||||
|
maybe_types = mt;
|
||||||
|
}
|
||||||
|
Some(dependencies)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let metadata = Metadata {
|
||||||
|
dependencies,
|
||||||
|
maybe_types,
|
||||||
|
media_type,
|
||||||
|
source,
|
||||||
|
version,
|
||||||
|
};
|
||||||
|
self.metadata.insert(specifier.clone(), metadata.clone());
|
||||||
|
Some(metadata)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_path(&mut self, specifier: &ModuleSpecifier) -> Option<PathBuf> {
|
||||||
|
let specifier = self.resolve_specifier(specifier)?;
|
||||||
|
if specifier.as_url().scheme() == "file" {
|
||||||
|
if let Ok(path) = specifier.as_url().to_file_path() {
|
||||||
|
Some(path)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else if let Some(path) = self.remotes.get(&specifier) {
|
||||||
|
Some(path.clone())
|
||||||
|
} else {
|
||||||
|
let path = self.http_cache.get_cache_filename(&specifier.as_url());
|
||||||
|
if path.is_file() {
|
||||||
|
self.remotes.insert(specifier.clone(), path.clone());
|
||||||
|
Some(path)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_remote_headers(
|
||||||
|
&self,
|
||||||
|
specifier: &ModuleSpecifier,
|
||||||
|
) -> Option<HashMap<String, String>> {
|
||||||
|
let cache_filename = self.http_cache.get_cache_filename(specifier.as_url());
|
||||||
|
let metadata_path = http_cache::Metadata::filename(&cache_filename);
|
||||||
|
if let Ok(metadata) = fs::read_to_string(metadata_path) {
|
||||||
|
if let Ok(metadata) =
|
||||||
|
serde_json::from_str::<'_, http_cache::Metadata>(&metadata)
|
||||||
|
{
|
||||||
|
return Some(metadata.headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_script_version(
|
||||||
|
&mut self,
|
||||||
|
specifier: &ModuleSpecifier,
|
||||||
|
) -> Option<String> {
|
||||||
|
if let Some(path) = self.get_path(specifier) {
|
||||||
|
if let Ok(metadata) = fs::metadata(path) {
|
||||||
|
if let Ok(modified) = metadata.modified() {
|
||||||
|
return if let Ok(n) = modified.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
{
|
||||||
|
Some(format!("{}", n.as_millis()))
|
||||||
|
} else {
|
||||||
|
Some("1".to_string())
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return Some("1".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_text(&mut self, specifier: &ModuleSpecifier) -> Option<String> {
|
||||||
|
let specifier = self.resolve_specifier(specifier)?;
|
||||||
|
let metadata = self.get_metadata(&specifier)?;
|
||||||
|
Some(metadata.source)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolution_result(
|
||||||
|
&mut self,
|
||||||
|
resolved_specifier: &ModuleSpecifier,
|
||||||
|
) -> Option<(ModuleSpecifier, MediaType)> {
|
||||||
|
let resolved_specifier = self.resolve_specifier(resolved_specifier)?;
|
||||||
|
let media_type =
|
||||||
|
if let Some(metadata) = self.metadata.get(&resolved_specifier) {
|
||||||
|
metadata.media_type
|
||||||
|
} else {
|
||||||
|
MediaType::from(&resolved_specifier)
|
||||||
|
};
|
||||||
|
Some((resolved_specifier, media_type))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_import(
|
||||||
|
&mut self,
|
||||||
|
specifier: &str,
|
||||||
|
referrer: &ModuleSpecifier,
|
||||||
|
) -> Option<(ModuleSpecifier, MediaType)> {
|
||||||
|
let referrer = self.resolve_specifier(referrer)?;
|
||||||
|
let metadata = self.get_metadata(&referrer)?;
|
||||||
|
let dependencies = &metadata.dependencies?;
|
||||||
|
let dependency = dependencies.get(specifier)?;
|
||||||
|
if let Some(type_dependency) = &dependency.maybe_type {
|
||||||
|
if let analysis::ResolvedImport::Resolved(resolved_specifier) =
|
||||||
|
type_dependency
|
||||||
|
{
|
||||||
|
self.resolution_result(resolved_specifier)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let code_dependency = &dependency.maybe_code.clone()?;
|
||||||
|
if let analysis::ResolvedImport::Resolved(resolved_specifier) =
|
||||||
|
code_dependency
|
||||||
|
{
|
||||||
|
self.resolution_result(resolved_specifier)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_specifier(
|
||||||
|
&mut self,
|
||||||
|
specifier: &ModuleSpecifier,
|
||||||
|
) -> Option<ModuleSpecifier> {
|
||||||
|
if specifier.as_url().scheme() == "file" {
|
||||||
|
if let Ok(path) = specifier.as_url().to_file_path() {
|
||||||
|
if path.is_file() {
|
||||||
|
return Some(specifier.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Some(specifier) = self.redirects.get(specifier) {
|
||||||
|
return Some(specifier.clone());
|
||||||
|
}
|
||||||
|
if let Some(redirect) = self.resolve_remote_specifier(specifier, 10) {
|
||||||
|
self.redirects.insert(specifier.clone(), redirect.clone());
|
||||||
|
return Some(redirect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_remote_specifier(
|
||||||
|
&self,
|
||||||
|
specifier: &ModuleSpecifier,
|
||||||
|
redirect_limit: isize,
|
||||||
|
) -> Option<ModuleSpecifier> {
|
||||||
|
let cached_filename =
|
||||||
|
self.http_cache.get_cache_filename(specifier.as_url());
|
||||||
|
if redirect_limit >= 0 && cached_filename.is_file() {
|
||||||
|
if let Some(headers) = self.get_remote_headers(specifier) {
|
||||||
|
if let Some(redirect_to) = headers.get("location") {
|
||||||
|
if let Ok(redirect) =
|
||||||
|
ModuleSpecifier::resolve_import(redirect_to, specifier.as_str())
|
||||||
|
{
|
||||||
|
return self
|
||||||
|
.resolve_remote_specifier(&redirect, redirect_limit - 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Some(specifier.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::env;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
fn setup() -> (Sources, PathBuf) {
|
||||||
|
let temp_dir = TempDir::new().expect("could not create temp dir");
|
||||||
|
let location = temp_dir.path().join("deps");
|
||||||
|
let sources = Sources::new(&location);
|
||||||
|
(sources, location)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sources_get_script_version() {
|
||||||
|
let (mut sources, _) = setup();
|
||||||
|
let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||||
|
let tests = c.join("tests");
|
||||||
|
let specifier = ModuleSpecifier::resolve_path(
|
||||||
|
&tests.join("001_hello.js").to_string_lossy(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let actual = sources.get_script_version(&specifier);
|
||||||
|
assert!(actual.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sources_get_text() {
|
||||||
|
let (mut sources, _) = setup();
|
||||||
|
let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||||
|
let tests = c.join("tests");
|
||||||
|
let specifier = ModuleSpecifier::resolve_path(
|
||||||
|
&tests.join("001_hello.js").to_string_lossy(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let actual = sources.get_text(&specifier);
|
||||||
|
assert!(actual.is_some());
|
||||||
|
let actual = actual.unwrap();
|
||||||
|
assert_eq!(actual, "console.log(\"Hello World\");\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sources_get_length() {
|
||||||
|
let (mut sources, _) = setup();
|
||||||
|
let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||||
|
let tests = c.join("tests");
|
||||||
|
let specifier = ModuleSpecifier::resolve_path(
|
||||||
|
&tests.join("001_hello.js").to_string_lossy(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let actual = sources.get_length(&specifier);
|
||||||
|
assert!(actual.is_some());
|
||||||
|
let actual = actual.unwrap();
|
||||||
|
assert_eq!(actual, 28);
|
||||||
|
}
|
||||||
|
}
|
292
cli/lsp/state.rs
Normal file
292
cli/lsp/state.rs
Normal file
|
@ -0,0 +1,292 @@
|
||||||
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
use super::analysis;
|
||||||
|
use super::config::Config;
|
||||||
|
use super::diagnostics::DiagnosticCollection;
|
||||||
|
use super::diagnostics::DiagnosticSource;
|
||||||
|
use super::diagnostics::DiagnosticVec;
|
||||||
|
use super::memory_cache::MemoryCache;
|
||||||
|
use super::sources::Sources;
|
||||||
|
use super::tsc;
|
||||||
|
use super::utils::notification_is;
|
||||||
|
|
||||||
|
use crate::deno_dir;
|
||||||
|
use crate::import_map::ImportMap;
|
||||||
|
use crate::media_type::MediaType;
|
||||||
|
|
||||||
|
use crossbeam_channel::select;
|
||||||
|
use crossbeam_channel::unbounded;
|
||||||
|
use crossbeam_channel::Receiver;
|
||||||
|
use crossbeam_channel::Sender;
|
||||||
|
use deno_core::JsRuntime;
|
||||||
|
use deno_core::ModuleSpecifier;
|
||||||
|
use lsp_server::Message;
|
||||||
|
use lsp_server::Notification;
|
||||||
|
use lsp_server::Request;
|
||||||
|
use lsp_server::RequestId;
|
||||||
|
use lsp_server::Response;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::env;
|
||||||
|
use std::fmt;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::RwLock;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
type ReqHandler = fn(&mut ServerState, Response);
|
||||||
|
type ReqQueue = lsp_server::ReqQueue<(String, Instant), ReqHandler>;
|
||||||
|
|
||||||
|
pub enum Event {
|
||||||
|
Message(Message),
|
||||||
|
Task(Task),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Event {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let debug_verbose_not =
|
||||||
|
|notification: &Notification, f: &mut fmt::Formatter| {
|
||||||
|
f.debug_struct("Notification")
|
||||||
|
.field("method", ¬ification.method)
|
||||||
|
.finish()
|
||||||
|
};
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Event::Message(Message::Notification(notification)) => {
|
||||||
|
if notification_is::<lsp_types::notification::DidOpenTextDocument>(
|
||||||
|
notification,
|
||||||
|
) || notification_is::<lsp_types::notification::DidChangeTextDocument>(
|
||||||
|
notification,
|
||||||
|
) {
|
||||||
|
return debug_verbose_not(notification, f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Task(Task::Response(response)) => {
|
||||||
|
return f
|
||||||
|
.debug_struct("Response")
|
||||||
|
.field("id", &response.id)
|
||||||
|
.field("error", &response.error)
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
match self {
|
||||||
|
Event::Message(it) => fmt::Debug::fmt(it, f),
|
||||||
|
Event::Task(it) => fmt::Debug::fmt(it, f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, Copy, Clone)]
|
||||||
|
pub enum Status {
|
||||||
|
Loading,
|
||||||
|
Ready,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Status {
|
||||||
|
fn default() -> Self {
|
||||||
|
Status::Loading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Task {
|
||||||
|
Diagnostics((DiagnosticSource, DiagnosticVec)),
|
||||||
|
Response(Response),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DocumentData {
|
||||||
|
pub dependencies: Option<HashMap<String, analysis::Dependency>>,
|
||||||
|
pub version: Option<i32>,
|
||||||
|
specifier: ModuleSpecifier,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DocumentData {
|
||||||
|
pub fn new(
|
||||||
|
specifier: ModuleSpecifier,
|
||||||
|
version: i32,
|
||||||
|
source: &str,
|
||||||
|
maybe_import_map: Option<Rc<RefCell<ImportMap>>>,
|
||||||
|
) -> Self {
|
||||||
|
let dependencies = if let Some((dependencies, _)) =
|
||||||
|
analysis::analyze_dependencies(
|
||||||
|
&specifier,
|
||||||
|
source,
|
||||||
|
&MediaType::from(&specifier),
|
||||||
|
maybe_import_map,
|
||||||
|
) {
|
||||||
|
Some(dependencies)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
dependencies,
|
||||||
|
version: Some(version),
|
||||||
|
specifier,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
version: i32,
|
||||||
|
source: &str,
|
||||||
|
maybe_import_map: Option<Rc<RefCell<ImportMap>>>,
|
||||||
|
) {
|
||||||
|
self.dependencies = if let Some((dependencies, _)) =
|
||||||
|
analysis::analyze_dependencies(
|
||||||
|
&self.specifier,
|
||||||
|
source,
|
||||||
|
&MediaType::from(&self.specifier),
|
||||||
|
maybe_import_map,
|
||||||
|
) {
|
||||||
|
Some(dependencies)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
self.version = Some(version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An immutable snapshot of the server state at a point in time.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ServerStateSnapshot {
|
||||||
|
pub config: Config,
|
||||||
|
pub diagnostics: DiagnosticCollection,
|
||||||
|
pub doc_data: HashMap<ModuleSpecifier, DocumentData>,
|
||||||
|
pub file_cache: Arc<RwLock<MemoryCache>>,
|
||||||
|
pub sources: Arc<RwLock<Sources>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ServerState {
|
||||||
|
pub config: Config,
|
||||||
|
pub diagnostics: DiagnosticCollection,
|
||||||
|
pub doc_data: HashMap<ModuleSpecifier, DocumentData>,
|
||||||
|
pub file_cache: Arc<RwLock<MemoryCache>>,
|
||||||
|
req_queue: ReqQueue,
|
||||||
|
sender: Sender<Message>,
|
||||||
|
pub sources: Arc<RwLock<Sources>>,
|
||||||
|
pub shutdown_requested: bool,
|
||||||
|
pub status: Status,
|
||||||
|
task_sender: Sender<Task>,
|
||||||
|
pub task_receiver: Receiver<Task>,
|
||||||
|
pub ts_runtime: JsRuntime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerState {
|
||||||
|
pub fn new(sender: Sender<Message>, config: Config) -> Self {
|
||||||
|
let (task_sender, task_receiver) = unbounded();
|
||||||
|
let custom_root = env::var("DENO_DIR").map(String::into).ok();
|
||||||
|
let dir =
|
||||||
|
deno_dir::DenoDir::new(custom_root).expect("could not access DENO_DIR");
|
||||||
|
let location = dir.root.join("deps");
|
||||||
|
let sources = Sources::new(&location);
|
||||||
|
// TODO(@kitsonk) we need to allow displaying diagnostics here, but the
|
||||||
|
// current compiler snapshot sends them to stdio which would totally break
|
||||||
|
// the language server...
|
||||||
|
let ts_runtime = tsc::start(false).expect("could not start tsc");
|
||||||
|
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
diagnostics: Default::default(),
|
||||||
|
doc_data: HashMap::new(),
|
||||||
|
file_cache: Arc::new(RwLock::new(Default::default())),
|
||||||
|
req_queue: Default::default(),
|
||||||
|
sender,
|
||||||
|
sources: Arc::new(RwLock::new(sources)),
|
||||||
|
shutdown_requested: false,
|
||||||
|
status: Default::default(),
|
||||||
|
task_receiver,
|
||||||
|
task_sender,
|
||||||
|
ts_runtime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cancel(&mut self, request_id: RequestId) {
|
||||||
|
if let Some(response) = self.req_queue.incoming.cancel(request_id) {
|
||||||
|
self.send(response.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn complete_request(&mut self, response: Response) {
|
||||||
|
let handler = self.req_queue.outgoing.complete(response.id.clone());
|
||||||
|
handler(self, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_event(&self, inbox: &Receiver<Message>) -> Option<Event> {
|
||||||
|
select! {
|
||||||
|
recv(inbox) -> msg => msg.ok().map(Event::Message),
|
||||||
|
recv(self.task_receiver) -> task => Some(Event::Task(task.unwrap())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle any changes and return a `bool` that indicates if there were
|
||||||
|
/// important changes to the state.
|
||||||
|
pub fn process_changes(&mut self) -> bool {
|
||||||
|
let mut file_cache = self.file_cache.write().unwrap();
|
||||||
|
let changed_files = file_cache.take_changes();
|
||||||
|
// other processing of changed files should be done here as needed
|
||||||
|
!changed_files.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_request(&mut self, request: &Request, received: Instant) {
|
||||||
|
self
|
||||||
|
.req_queue
|
||||||
|
.incoming
|
||||||
|
.register(request.id.clone(), (request.method.clone(), received));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn respond(&mut self, response: Response) {
|
||||||
|
if let Some((_, _)) = self.req_queue.incoming.complete(response.id.clone())
|
||||||
|
{
|
||||||
|
self.send(response.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send(&mut self, message: Message) {
|
||||||
|
self.sender.send(message).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_notification<N: lsp_types::notification::Notification>(
|
||||||
|
&mut self,
|
||||||
|
params: N::Params,
|
||||||
|
) {
|
||||||
|
let notification = Notification::new(N::METHOD.to_string(), params);
|
||||||
|
self.send(notification.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_request<R: lsp_types::request::Request>(
|
||||||
|
&mut self,
|
||||||
|
params: R::Params,
|
||||||
|
handler: ReqHandler,
|
||||||
|
) {
|
||||||
|
let request =
|
||||||
|
self
|
||||||
|
.req_queue
|
||||||
|
.outgoing
|
||||||
|
.register(R::METHOD.to_string(), params, handler);
|
||||||
|
self.send(request.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn snapshot(&self) -> ServerStateSnapshot {
|
||||||
|
ServerStateSnapshot {
|
||||||
|
config: self.config.clone(),
|
||||||
|
diagnostics: self.diagnostics.clone(),
|
||||||
|
doc_data: self.doc_data.clone(),
|
||||||
|
file_cache: Arc::clone(&self.file_cache),
|
||||||
|
sources: Arc::clone(&self.sources),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn<F>(&mut self, task: F)
|
||||||
|
where
|
||||||
|
F: FnOnce() -> Task + Send + 'static,
|
||||||
|
{
|
||||||
|
let sender = self.task_sender.clone();
|
||||||
|
tokio::task::spawn_blocking(move || sender.send(task()).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transition(&mut self, new_status: Status) {
|
||||||
|
self.status = new_status;
|
||||||
|
}
|
||||||
|
}
|
514
cli/lsp/text.rs
Normal file
514
cli/lsp/text.rs
Normal file
|
@ -0,0 +1,514 @@
|
||||||
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
use deno_core::serde_json::json;
|
||||||
|
use deno_core::serde_json::Value;
|
||||||
|
use dissimilar::diff;
|
||||||
|
use dissimilar::Chunk;
|
||||||
|
use lsp_types::TextEdit;
|
||||||
|
use std::ops::Bound;
|
||||||
|
use std::ops::Range;
|
||||||
|
use std::ops::RangeBounds;
|
||||||
|
|
||||||
|
// TODO(@kitson) in general all of these text handling routines don't handle
|
||||||
|
// JavaScript encoding in the same way and likely cause issues when trying to
|
||||||
|
// arbitrate between chars and Unicode graphemes. There be dragons.
|
||||||
|
|
||||||
|
/// Generate a character position for the start of each line. For example:
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// let actual = index_lines("a\nb\n");
|
||||||
|
/// assert_eq!(actual, vec![0, 2, 4]);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
pub fn index_lines(text: &str) -> Vec<u32> {
|
||||||
|
let mut indexes = vec![0_u32];
|
||||||
|
for (i, c) in text.chars().enumerate() {
|
||||||
|
if c == '\n' {
|
||||||
|
indexes.push((i + 1) as u32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
indexes
|
||||||
|
}
|
||||||
|
|
||||||
|
enum IndexValid {
|
||||||
|
All,
|
||||||
|
UpTo(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IndexValid {
|
||||||
|
fn covers(&self, line: u32) -> bool {
|
||||||
|
match *self {
|
||||||
|
IndexValid::UpTo(to) => to > line,
|
||||||
|
IndexValid::All => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_range(line_index: &[u32], range: lsp_types::Range) -> Range<usize> {
|
||||||
|
let start =
|
||||||
|
(line_index[range.start.line as usize] + range.start.character) as usize;
|
||||||
|
let end =
|
||||||
|
(line_index[range.end.line as usize] + range.end.character) as usize;
|
||||||
|
Range { start, end }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_position(line_index: &[u32], char_pos: u32) -> lsp_types::Position {
|
||||||
|
let mut line = 0_usize;
|
||||||
|
let mut line_start = 0_u32;
|
||||||
|
for (pos, v) in line_index.iter().enumerate() {
|
||||||
|
if char_pos < *v {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
line_start = *v;
|
||||||
|
line = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
lsp_types::Position {
|
||||||
|
line: line as u32,
|
||||||
|
character: char_pos - line_start,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_char_pos(line_index: &[u32], position: lsp_types::Position) -> u32 {
|
||||||
|
if let Some(line_start) = line_index.get(position.line as usize) {
|
||||||
|
line_start + position.character
|
||||||
|
} else {
|
||||||
|
0_u32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply a vector of document changes to the supplied string.
|
||||||
|
pub fn apply_content_changes(
|
||||||
|
content: &mut String,
|
||||||
|
content_changes: Vec<lsp_types::TextDocumentContentChangeEvent>,
|
||||||
|
) {
|
||||||
|
let mut line_index = index_lines(&content);
|
||||||
|
let mut index_valid = IndexValid::All;
|
||||||
|
for change in content_changes {
|
||||||
|
if let Some(range) = change.range {
|
||||||
|
if !index_valid.covers(range.start.line) {
|
||||||
|
line_index = index_lines(&content);
|
||||||
|
}
|
||||||
|
let range = to_range(&line_index, range);
|
||||||
|
content.replace_range(range, &change.text);
|
||||||
|
} else {
|
||||||
|
*content = change.text;
|
||||||
|
index_valid = IndexValid::UpTo(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compare two strings and return a vector of text edit records which are
|
||||||
|
/// supported by the Language Server Protocol.
|
||||||
|
pub fn get_edits(a: &str, b: &str) -> Vec<TextEdit> {
|
||||||
|
let chunks = diff(a, b);
|
||||||
|
let mut text_edits = Vec::<TextEdit>::new();
|
||||||
|
let line_index = index_lines(a);
|
||||||
|
let mut iter = chunks.iter().peekable();
|
||||||
|
let mut a_pos = 0_u32;
|
||||||
|
loop {
|
||||||
|
let chunk = iter.next();
|
||||||
|
match chunk {
|
||||||
|
None => break,
|
||||||
|
Some(Chunk::Equal(e)) => {
|
||||||
|
a_pos += e.chars().count() as u32;
|
||||||
|
}
|
||||||
|
Some(Chunk::Delete(d)) => {
|
||||||
|
let start = to_position(&line_index, a_pos);
|
||||||
|
a_pos += d.chars().count() as u32;
|
||||||
|
let end = to_position(&line_index, a_pos);
|
||||||
|
let range = lsp_types::Range { start, end };
|
||||||
|
match iter.peek() {
|
||||||
|
Some(Chunk::Insert(i)) => {
|
||||||
|
iter.next();
|
||||||
|
text_edits.push(TextEdit {
|
||||||
|
range,
|
||||||
|
new_text: i.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => text_edits.push(TextEdit {
|
||||||
|
range,
|
||||||
|
new_text: "".to_string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Chunk::Insert(i)) => {
|
||||||
|
let pos = to_position(&line_index, a_pos);
|
||||||
|
let range = lsp_types::Range {
|
||||||
|
start: pos,
|
||||||
|
end: pos,
|
||||||
|
};
|
||||||
|
text_edits.push(TextEdit {
|
||||||
|
range,
|
||||||
|
new_text: i.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text_edits
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a difference between two strings into a change range used by the
|
||||||
|
/// TypeScript Language Service.
|
||||||
|
pub fn get_range_change(a: &str, b: &str) -> Value {
|
||||||
|
let chunks = diff(a, b);
|
||||||
|
let mut iter = chunks.iter().peekable();
|
||||||
|
let mut started = false;
|
||||||
|
let mut start = 0;
|
||||||
|
let mut end = 0;
|
||||||
|
let mut new_length = 0;
|
||||||
|
let mut equal = 0;
|
||||||
|
let mut a_pos = 0;
|
||||||
|
loop {
|
||||||
|
let chunk = iter.next();
|
||||||
|
match chunk {
|
||||||
|
None => break,
|
||||||
|
Some(Chunk::Equal(e)) => {
|
||||||
|
a_pos += e.chars().count();
|
||||||
|
equal += e.chars().count();
|
||||||
|
}
|
||||||
|
Some(Chunk::Delete(d)) => {
|
||||||
|
if !started {
|
||||||
|
start = a_pos;
|
||||||
|
started = true;
|
||||||
|
equal = 0;
|
||||||
|
}
|
||||||
|
a_pos += d.chars().count();
|
||||||
|
if started {
|
||||||
|
end = a_pos;
|
||||||
|
new_length += equal;
|
||||||
|
equal = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Chunk::Insert(i)) => {
|
||||||
|
if !started {
|
||||||
|
start = a_pos;
|
||||||
|
end = a_pos;
|
||||||
|
started = true;
|
||||||
|
equal = 0;
|
||||||
|
} else {
|
||||||
|
end += equal;
|
||||||
|
}
|
||||||
|
new_length += i.chars().count() + equal;
|
||||||
|
equal = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
json!({
|
||||||
|
"span": {
|
||||||
|
"start": start,
|
||||||
|
"length": end - start,
|
||||||
|
},
|
||||||
|
"newLength": new_length,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provide a slice of a string based on a character range.
|
||||||
|
pub fn slice(s: &str, range: impl RangeBounds<usize>) -> &str {
|
||||||
|
let start = match range.start_bound() {
|
||||||
|
Bound::Included(bound) | Bound::Excluded(bound) => *bound,
|
||||||
|
Bound::Unbounded => 0,
|
||||||
|
};
|
||||||
|
let len = match range.end_bound() {
|
||||||
|
Bound::Included(bound) => *bound + 1,
|
||||||
|
Bound::Excluded(bound) => *bound,
|
||||||
|
Bound::Unbounded => s.len(),
|
||||||
|
} - start;
|
||||||
|
substring(s, start, start + len)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provide a substring based on the start and end character index positions.
|
||||||
|
pub fn substring(s: &str, start: usize, end: usize) -> &str {
|
||||||
|
let len = end - start;
|
||||||
|
let mut char_pos = 0;
|
||||||
|
let mut byte_start = 0;
|
||||||
|
let mut it = s.chars();
|
||||||
|
loop {
|
||||||
|
if char_pos == start {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Some(c) = it.next() {
|
||||||
|
char_pos += 1;
|
||||||
|
byte_start += c.len_utf8();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
char_pos = 0;
|
||||||
|
let mut byte_end = byte_start;
|
||||||
|
loop {
|
||||||
|
if char_pos == len {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Some(c) = it.next() {
|
||||||
|
char_pos += 1;
|
||||||
|
byte_end += c.len_utf8();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&s[byte_start..byte_end]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_apply_content_changes() {
|
||||||
|
let mut content = "a\nb\nc\nd".to_string();
|
||||||
|
let content_changes = vec![lsp_types::TextDocumentContentChangeEvent {
|
||||||
|
range: Some(lsp_types::Range {
|
||||||
|
start: lsp_types::Position {
|
||||||
|
line: 1,
|
||||||
|
character: 0,
|
||||||
|
},
|
||||||
|
end: lsp_types::Position {
|
||||||
|
line: 1,
|
||||||
|
character: 1,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
range_length: Some(1),
|
||||||
|
text: "e".to_string(),
|
||||||
|
}];
|
||||||
|
apply_content_changes(&mut content, content_changes);
|
||||||
|
assert_eq!(content, "a\ne\nc\nd");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_edits() {
|
||||||
|
let a = "abcdefg";
|
||||||
|
let b = "a\nb\nchije\nfg\n";
|
||||||
|
let actual = get_edits(a, b);
|
||||||
|
assert_eq!(
|
||||||
|
actual,
|
||||||
|
vec![
|
||||||
|
TextEdit {
|
||||||
|
range: lsp_types::Range {
|
||||||
|
start: lsp_types::Position {
|
||||||
|
line: 0,
|
||||||
|
character: 1
|
||||||
|
},
|
||||||
|
end: lsp_types::Position {
|
||||||
|
line: 0,
|
||||||
|
character: 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new_text: "\nb\nchije\n".to_string()
|
||||||
|
},
|
||||||
|
TextEdit {
|
||||||
|
range: lsp_types::Range {
|
||||||
|
start: lsp_types::Position {
|
||||||
|
line: 0,
|
||||||
|
character: 7
|
||||||
|
},
|
||||||
|
end: lsp_types::Position {
|
||||||
|
line: 0,
|
||||||
|
character: 7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new_text: "\n".to_string()
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_range_change() {
|
||||||
|
let a = "abcdefg";
|
||||||
|
let b = "abedcfg";
|
||||||
|
let actual = get_range_change(a, b);
|
||||||
|
assert_eq!(
|
||||||
|
actual,
|
||||||
|
json!({
|
||||||
|
"span": {
|
||||||
|
"start": 2,
|
||||||
|
"length": 3,
|
||||||
|
},
|
||||||
|
"newLength": 3
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let a = "abfg";
|
||||||
|
let b = "abcdefg";
|
||||||
|
let actual = get_range_change(a, b);
|
||||||
|
assert_eq!(
|
||||||
|
actual,
|
||||||
|
json!({
|
||||||
|
"span": {
|
||||||
|
"start": 2,
|
||||||
|
"length": 0,
|
||||||
|
},
|
||||||
|
"newLength": 3
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let a = "abcdefg";
|
||||||
|
let b = "abfg";
|
||||||
|
let actual = get_range_change(a, b);
|
||||||
|
assert_eq!(
|
||||||
|
actual,
|
||||||
|
json!({
|
||||||
|
"span": {
|
||||||
|
"start": 2,
|
||||||
|
"length": 3,
|
||||||
|
},
|
||||||
|
"newLength": 0
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let a = "abcdefg";
|
||||||
|
let b = "abfghij";
|
||||||
|
let actual = get_range_change(a, b);
|
||||||
|
assert_eq!(
|
||||||
|
actual,
|
||||||
|
json!({
|
||||||
|
"span": {
|
||||||
|
"start": 2,
|
||||||
|
"length": 5,
|
||||||
|
},
|
||||||
|
"newLength": 5
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let a = "abcdefghijk";
|
||||||
|
let b = "axcxexfxixk";
|
||||||
|
let actual = get_range_change(a, b);
|
||||||
|
assert_eq!(
|
||||||
|
actual,
|
||||||
|
json!({
|
||||||
|
"span": {
|
||||||
|
"start": 1,
|
||||||
|
"length": 9,
|
||||||
|
},
|
||||||
|
"newLength": 9
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let a = "abcde";
|
||||||
|
let b = "ab(c)de";
|
||||||
|
let actual = get_range_change(a, b);
|
||||||
|
assert_eq!(
|
||||||
|
actual,
|
||||||
|
json!({
|
||||||
|
"span" : {
|
||||||
|
"start": 2,
|
||||||
|
"length": 1,
|
||||||
|
},
|
||||||
|
"newLength": 3
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_index_lines() {
|
||||||
|
let actual = index_lines("a\nb\r\nc");
|
||||||
|
assert_eq!(actual, vec![0, 2, 5]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_to_position() {
|
||||||
|
let line_index = index_lines("a\nb\r\nc\n");
|
||||||
|
assert_eq!(
|
||||||
|
to_position(&line_index, 6),
|
||||||
|
lsp_types::Position {
|
||||||
|
line: 2,
|
||||||
|
character: 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
to_position(&line_index, 0),
|
||||||
|
lsp_types::Position {
|
||||||
|
line: 0,
|
||||||
|
character: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
to_position(&line_index, 3),
|
||||||
|
lsp_types::Position {
|
||||||
|
line: 1,
|
||||||
|
character: 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_to_position_mbc() {
|
||||||
|
let line_index = index_lines("y̆\n😱🦕\n🤯\n");
|
||||||
|
assert_eq!(
|
||||||
|
to_position(&line_index, 0),
|
||||||
|
lsp_types::Position {
|
||||||
|
line: 0,
|
||||||
|
character: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
to_position(&line_index, 2),
|
||||||
|
lsp_types::Position {
|
||||||
|
line: 0,
|
||||||
|
character: 2,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
to_position(&line_index, 3),
|
||||||
|
lsp_types::Position {
|
||||||
|
line: 1,
|
||||||
|
character: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
to_position(&line_index, 4),
|
||||||
|
lsp_types::Position {
|
||||||
|
line: 1,
|
||||||
|
character: 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
to_position(&line_index, 5),
|
||||||
|
lsp_types::Position {
|
||||||
|
line: 1,
|
||||||
|
character: 2,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
to_position(&line_index, 6),
|
||||||
|
lsp_types::Position {
|
||||||
|
line: 2,
|
||||||
|
character: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
to_position(&line_index, 7),
|
||||||
|
lsp_types::Position {
|
||||||
|
line: 2,
|
||||||
|
character: 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
to_position(&line_index, 8),
|
||||||
|
lsp_types::Position {
|
||||||
|
line: 3,
|
||||||
|
character: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_substring() {
|
||||||
|
assert_eq!(substring("Deno", 1, 3), "en");
|
||||||
|
assert_eq!(substring("y̆y̆", 2, 4), "y̆");
|
||||||
|
// this doesn't work like JavaScript, as 🦕 is treated as a single char in
|
||||||
|
// Rust, but as two chars in JavaScript.
|
||||||
|
// assert_eq!(substring("🦕🦕", 2, 4), "🦕");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_slice() {
|
||||||
|
assert_eq!(slice("Deno", 1..3), "en");
|
||||||
|
assert_eq!(slice("Deno", 1..=3), "eno");
|
||||||
|
assert_eq!(slice("Deno Land", 1..), "eno Land");
|
||||||
|
assert_eq!(slice("Deno", ..3), "Den");
|
||||||
|
}
|
||||||
|
}
|
1210
cli/lsp/tsc.rs
Normal file
1210
cli/lsp/tsc.rs
Normal file
File diff suppressed because it is too large
Load diff
114
cli/lsp/utils.rs
Normal file
114
cli/lsp/utils.rs
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
use deno_core::error::custom_error;
|
||||||
|
use deno_core::error::AnyError;
|
||||||
|
use deno_core::serde_json::Value;
|
||||||
|
use deno_core::url::Position;
|
||||||
|
use deno_core::url::Url;
|
||||||
|
use deno_core::ModuleSpecifier;
|
||||||
|
use lsp_server::Notification;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
// TODO(@kitsonk) support actually supporting cancellation requests from the
|
||||||
|
// client.
|
||||||
|
|
||||||
|
pub struct Canceled {
|
||||||
|
_private: (),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Canceled {
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { _private: () }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn throw() -> ! {
|
||||||
|
std::panic::resume_unwind(Box::new(Canceled::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Canceled {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "cancelled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Canceled {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "Canceled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for Canceled {}
|
||||||
|
|
||||||
|
pub fn from_json<T: DeserializeOwned>(
|
||||||
|
what: &'static str,
|
||||||
|
json: Value,
|
||||||
|
) -> Result<T, AnyError> {
|
||||||
|
let response = T::deserialize(&json).map_err(|err| {
|
||||||
|
custom_error(
|
||||||
|
"DeserializeFailed",
|
||||||
|
format!("Failed to deserialize {}: {}; {}", what, err, json),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_canceled(e: &(dyn Error + 'static)) -> bool {
|
||||||
|
e.downcast_ref::<Canceled>().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notification_is<N: lsp_types::notification::Notification>(
|
||||||
|
notification: &Notification,
|
||||||
|
) -> bool {
|
||||||
|
notification.method == N::METHOD
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Normalizes a file name returned from the TypeScript compiler into a URI that
|
||||||
|
/// should be sent by the language server to the client.
|
||||||
|
pub fn normalize_file_name(file_name: &str) -> Result<Url, AnyError> {
|
||||||
|
let specifier_str = if file_name.starts_with("file://") {
|
||||||
|
file_name.to_string()
|
||||||
|
} else {
|
||||||
|
format!("deno:///{}", file_name.replacen("://", "/", 1))
|
||||||
|
};
|
||||||
|
Url::parse(&specifier_str).map_err(|err| err.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Normalize URLs from the client, where "virtual" `deno:///` URLs are
|
||||||
|
/// converted into proper module specifiers.
|
||||||
|
pub fn normalize_url(url: Url) -> ModuleSpecifier {
|
||||||
|
if url.scheme() == "deno"
|
||||||
|
&& (url.path().starts_with("/http") || url.path().starts_with("/asset"))
|
||||||
|
{
|
||||||
|
let specifier_str = url[Position::BeforePath..]
|
||||||
|
.replacen("/", "", 1)
|
||||||
|
.replacen("/", "://", 1);
|
||||||
|
if let Ok(specifier) =
|
||||||
|
percent_encoding::percent_decode_str(&specifier_str).decode_utf8()
|
||||||
|
{
|
||||||
|
if let Ok(specifier) = ModuleSpecifier::resolve_url(&specifier) {
|
||||||
|
return specifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ModuleSpecifier::from(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_normalize_url() {
|
||||||
|
let fixture = Url::parse("deno:///https/deno.land/x/mod.ts").unwrap();
|
||||||
|
let actual = normalize_url(fixture);
|
||||||
|
assert_eq!(
|
||||||
|
actual,
|
||||||
|
ModuleSpecifier::resolve_url("https://deno.land/x/mod.ts").unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ mod info;
|
||||||
mod inspector;
|
mod inspector;
|
||||||
mod js;
|
mod js;
|
||||||
mod lockfile;
|
mod lockfile;
|
||||||
|
mod lsp;
|
||||||
mod media_type;
|
mod media_type;
|
||||||
mod metrics;
|
mod metrics;
|
||||||
mod module_graph;
|
mod module_graph;
|
||||||
|
@ -258,6 +259,10 @@ async fn install_command(
|
||||||
tools::installer::install(flags, &module_url, args, name, root, force)
|
tools::installer::install(flags, &module_url, args, name, root, force)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn language_server_command() -> Result<(), AnyError> {
|
||||||
|
lsp::start()
|
||||||
|
}
|
||||||
|
|
||||||
async fn lint_command(
|
async fn lint_command(
|
||||||
flags: Flags,
|
flags: Flags,
|
||||||
files: Vec<PathBuf>,
|
files: Vec<PathBuf>,
|
||||||
|
@ -992,6 +997,7 @@ fn get_subcommand(
|
||||||
} => {
|
} => {
|
||||||
install_command(flags, module_url, args, name, root, force).boxed_local()
|
install_command(flags, module_url, args, name, root, force).boxed_local()
|
||||||
}
|
}
|
||||||
|
DenoSubcommand::LanguageServer => language_server_command().boxed_local(),
|
||||||
DenoSubcommand::Lint {
|
DenoSubcommand::Lint {
|
||||||
files,
|
files,
|
||||||
rules,
|
rules,
|
||||||
|
|
|
@ -31,6 +31,8 @@ use crate::AnyError;
|
||||||
use deno_core::error::Context;
|
use deno_core::error::Context;
|
||||||
use deno_core::futures::stream::FuturesUnordered;
|
use deno_core::futures::stream::FuturesUnordered;
|
||||||
use deno_core::futures::stream::StreamExt;
|
use deno_core::futures::stream::StreamExt;
|
||||||
|
use deno_core::serde::Deserialize;
|
||||||
|
use deno_core::serde::Deserializer;
|
||||||
use deno_core::serde::Serialize;
|
use deno_core::serde::Serialize;
|
||||||
use deno_core::serde::Serializer;
|
use deno_core::serde::Serializer;
|
||||||
use deno_core::serde_json::json;
|
use deno_core::serde_json::json;
|
||||||
|
@ -38,8 +40,6 @@ use deno_core::serde_json::Value;
|
||||||
use deno_core::ModuleResolutionError;
|
use deno_core::ModuleResolutionError;
|
||||||
use deno_core::ModuleSpecifier;
|
use deno_core::ModuleSpecifier;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Deserialize;
|
|
||||||
use serde::Deserializer;
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::collections::{BTreeSet, HashMap};
|
use std::collections::{BTreeSet, HashMap};
|
||||||
|
@ -182,14 +182,14 @@ impl swc_bundler::Load for BundleLoader<'_> {
|
||||||
|
|
||||||
/// An enum which represents the parsed out values of references in source code.
|
/// An enum which represents the parsed out values of references in source code.
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
enum TypeScriptReference {
|
pub enum TypeScriptReference {
|
||||||
Path(String),
|
Path(String),
|
||||||
Types(String),
|
Types(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determine if a comment contains a triple slash reference and optionally
|
/// Determine if a comment contains a triple slash reference and optionally
|
||||||
/// return its kind and value.
|
/// return its kind and value.
|
||||||
fn parse_ts_reference(comment: &str) -> Option<TypeScriptReference> {
|
pub fn parse_ts_reference(comment: &str) -> Option<TypeScriptReference> {
|
||||||
if !TRIPLE_SLASH_REFERENCE_RE.is_match(comment) {
|
if !TRIPLE_SLASH_REFERENCE_RE.is_match(comment) {
|
||||||
None
|
None
|
||||||
} else if let Some(captures) = PATH_REFERENCE_RE.captures(comment) {
|
} else if let Some(captures) = PATH_REFERENCE_RE.captures(comment) {
|
||||||
|
@ -207,7 +207,7 @@ fn parse_ts_reference(comment: &str) -> Option<TypeScriptReference> {
|
||||||
|
|
||||||
/// Determine if a comment contains a `@deno-types` pragma and optionally return
|
/// Determine if a comment contains a `@deno-types` pragma and optionally return
|
||||||
/// its value.
|
/// its value.
|
||||||
fn parse_deno_types(comment: &str) -> Option<String> {
|
pub fn parse_deno_types(comment: &str) -> Option<String> {
|
||||||
if let Some(captures) = DENO_TYPES_RE.captures(comment) {
|
if let Some(captures) = DENO_TYPES_RE.captures(comment) {
|
||||||
if let Some(m) = captures.get(1) {
|
if let Some(m) = captures.get(1) {
|
||||||
Some(m.as_str().to_string())
|
Some(m.as_str().to_string())
|
||||||
|
@ -230,8 +230,8 @@ fn get_version(source: &str, version: &str, config: &[u8]) -> String {
|
||||||
|
|
||||||
/// A logical representation of a module within a graph.
|
/// A logical representation of a module within a graph.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct Module {
|
pub struct Module {
|
||||||
dependencies: DependencyMap,
|
pub dependencies: DependencyMap,
|
||||||
is_dirty: bool,
|
is_dirty: bool,
|
||||||
is_parsed: bool,
|
is_parsed: bool,
|
||||||
maybe_emit: Option<Emit>,
|
maybe_emit: Option<Emit>,
|
||||||
|
|
|
@ -914,7 +914,7 @@ fn ts_reload() {
|
||||||
assert!(std::str::from_utf8(&output.stdout)
|
assert!(std::str::from_utf8(&output.stdout)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.trim()
|
.trim()
|
||||||
.contains("\"host.writeFile(\\\"deno://002_hello.js\\\")\""));
|
.contains("host.writeFile(\"deno://002_hello.js\")"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
12
cli/tests/lsp/did_open_notification.json
Normal file
12
cli/tests/lsp/did_open_notification.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "textDocument/didOpen",
|
||||||
|
"params": {
|
||||||
|
"textDocument": {
|
||||||
|
"uri": "file:///a/file.ts",
|
||||||
|
"languageId": "typescript",
|
||||||
|
"version": 1,
|
||||||
|
"text": "console.log(Deno.args);\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
cli/tests/lsp/exit_notification.json
Normal file
5
cli/tests/lsp/exit_notification.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "exit",
|
||||||
|
"params": null
|
||||||
|
}
|
14
cli/tests/lsp/hover_request.json
Normal file
14
cli/tests/lsp/hover_request.json
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 2,
|
||||||
|
"method": "textDocument/hover",
|
||||||
|
"params": {
|
||||||
|
"textDocument": {
|
||||||
|
"uri": "file:///a/file.ts"
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"line": 0,
|
||||||
|
"character": 19
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
cli/tests/lsp/initialize_request.json
Normal file
23
cli/tests/lsp/initialize_request.json
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"method": "initialize",
|
||||||
|
"params": {
|
||||||
|
"processId": 0,
|
||||||
|
"clientInfo": {
|
||||||
|
"name": "test-harness",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"rootUri": null,
|
||||||
|
"capabilities": {
|
||||||
|
"textDocument": {
|
||||||
|
"synchronization": {
|
||||||
|
"dynamicRegistration": true,
|
||||||
|
"willSave": true,
|
||||||
|
"willSaveWaitUntil": true,
|
||||||
|
"didSave": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
cli/tests/lsp/initialized_notification.json
Normal file
5
cli/tests/lsp/initialized_notification.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "initialized",
|
||||||
|
"params": {}
|
||||||
|
}
|
6
cli/tests/lsp/shutdown_request.json
Normal file
6
cli/tests/lsp/shutdown_request.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 3,
|
||||||
|
"method": "shutdown",
|
||||||
|
"params": null
|
||||||
|
}
|
88
cli/tests/lsp_tests.rs
Normal file
88
cli/tests/lsp_tests.rs
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
///!
|
||||||
|
///! Integration test for the Deno Language Server (`deno lsp`)
|
||||||
|
///!
|
||||||
|
use std::fs;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::Stdio;
|
||||||
|
|
||||||
|
struct LspIntegrationTest {
|
||||||
|
pub fixtures: Vec<&'static str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LspIntegrationTest {
|
||||||
|
pub fn run(&self) -> (String, String) {
|
||||||
|
let root_path = test_util::root_path();
|
||||||
|
let deno_exe = test_util::deno_exe_path();
|
||||||
|
let tests_dir = root_path.join("cli/tests/lsp");
|
||||||
|
println!("tests_dir: {:?} deno_exe: {:?}", tests_dir, deno_exe);
|
||||||
|
let mut command = test_util::deno_cmd();
|
||||||
|
command
|
||||||
|
.arg("lsp")
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped());
|
||||||
|
|
||||||
|
let process = command.spawn().expect("failed to execute deno");
|
||||||
|
|
||||||
|
for fixture in &self.fixtures {
|
||||||
|
let mut stdin = process.stdin.as_ref().unwrap();
|
||||||
|
let fixture_path = tests_dir.join(fixture);
|
||||||
|
let content =
|
||||||
|
fs::read_to_string(&fixture_path).expect("could not read fixture");
|
||||||
|
let content_length = content.chars().count();
|
||||||
|
write!(
|
||||||
|
stdin,
|
||||||
|
"Content-Length: {}\r\n\r\n{}",
|
||||||
|
content_length, content
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut so = String::new();
|
||||||
|
process.stdout.unwrap().read_to_string(&mut so).unwrap();
|
||||||
|
|
||||||
|
let mut se = String::new();
|
||||||
|
process.stderr.unwrap().read_to_string(&mut se).unwrap();
|
||||||
|
|
||||||
|
(so, se)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lsp_startup_shutdown() {
|
||||||
|
let test = LspIntegrationTest {
|
||||||
|
fixtures: vec![
|
||||||
|
"initialize_request.json",
|
||||||
|
"initialized_notification.json",
|
||||||
|
"shutdown_request.json",
|
||||||
|
"exit_notification.json",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let (response, out) = test.run();
|
||||||
|
assert!(response.contains("deno-language-server"));
|
||||||
|
assert!(out.contains("Connected to \"test-harness\" 1.0.0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lsp_hover() {
|
||||||
|
// a straight forward integration tests starts up the lsp, opens a document
|
||||||
|
// which logs `Deno.args` to the console, and hovers over the `args` property
|
||||||
|
// to get the intellisense about it, which is a total end-to-end test that
|
||||||
|
// includes sending information in and out of the TypeScript compiler.
|
||||||
|
let test = LspIntegrationTest {
|
||||||
|
fixtures: vec![
|
||||||
|
"initialize_request.json",
|
||||||
|
"initialized_notification.json",
|
||||||
|
"did_open_notification.json",
|
||||||
|
"hover_request.json",
|
||||||
|
"shutdown_request.json",
|
||||||
|
"exit_notification.json",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let (response, out) = test.run();
|
||||||
|
assert!(response.contains("const Deno.args: string[]"));
|
||||||
|
assert!(out.contains("Connected to \"test-harness\" 1.0.0"));
|
||||||
|
}
|
|
@ -1,3 +1,3 @@
|
||||||
[WILDCARD]
|
[WILDCARD]
|
||||||
DEBUG TS - "host.getSourceFile(\"http://127.0.0.1:4545/xTypeScriptTypes.d.ts\", Latest)"
|
DEBUG TS - host.getSourceFile("http://127.0.0.1:4545/xTypeScriptTypes.d.ts", Latest)
|
||||||
[WILDCARD]
|
[WILDCARD]
|
|
@ -1,3 +1,3 @@
|
||||||
[WILDCARD]
|
[WILDCARD]
|
||||||
DEBUG TS - "host.getSourceFile(\"file:///[WILDCARD]cli/tests/subdir/type_reference.d.ts\", Latest)"
|
DEBUG TS - host.getSourceFile("file:///[WILDCARD]cli/tests/subdir/type_reference.d.ts", Latest)
|
||||||
[WILDCARD]
|
[WILDCARD]
|
|
@ -122,7 +122,7 @@ pub fn print_rules_list(json: bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_linter(syntax: Syntax, rules: Vec<Box<dyn LintRule>>) -> Linter {
|
pub fn create_linter(syntax: Syntax, rules: Vec<Box<dyn LintRule>>) -> Linter {
|
||||||
LinterBuilder::default()
|
LinterBuilder::default()
|
||||||
.ignore_file_directive("deno-lint-ignore-file")
|
.ignore_file_directive("deno-lint-ignore-file")
|
||||||
.ignore_diagnostic_directive("deno-lint-ignore")
|
.ignore_diagnostic_directive("deno-lint-ignore")
|
||||||
|
|
|
@ -284,12 +284,12 @@ fn load(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct ResolveArgs {
|
pub struct ResolveArgs {
|
||||||
/// The base specifier that the supplied specifier strings should be resolved
|
/// The base specifier that the supplied specifier strings should be resolved
|
||||||
/// relative to.
|
/// relative to.
|
||||||
base: String,
|
pub base: String,
|
||||||
/// A list of specifiers that should be resolved.
|
/// A list of specifiers that should be resolved.
|
||||||
specifiers: Vec<String>,
|
pub specifiers: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
// @ts-check
|
||||||
|
/// <reference path="./compiler.d.ts" />
|
||||||
// deno-lint-ignore-file no-undef
|
// deno-lint-ignore-file no-undef
|
||||||
|
|
||||||
// This module is the entry point for "compiler" isolate, ie. the one
|
// This module is the entry point for "compiler" isolate, ie. the one
|
||||||
|
@ -11,6 +13,7 @@
|
||||||
delete Object.prototype.__proto__;
|
delete Object.prototype.__proto__;
|
||||||
|
|
||||||
((window) => {
|
((window) => {
|
||||||
|
/** @type {DenoCore} */
|
||||||
const core = window.Deno.core;
|
const core = window.Deno.core;
|
||||||
|
|
||||||
let logDebug = false;
|
let logDebug = false;
|
||||||
|
@ -25,7 +28,9 @@ delete Object.prototype.__proto__;
|
||||||
|
|
||||||
function debug(...args) {
|
function debug(...args) {
|
||||||
if (logDebug) {
|
if (logDebug) {
|
||||||
const stringifiedArgs = args.map((arg) => JSON.stringify(arg)).join(" ");
|
const stringifiedArgs = args.map((arg) =>
|
||||||
|
typeof arg === "string" ? arg : JSON.stringify(arg)
|
||||||
|
).join(" ");
|
||||||
core.print(`DEBUG ${logSource} - ${stringifiedArgs}\n`);
|
core.print(`DEBUG ${logSource} - ${stringifiedArgs}\n`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,6 +91,7 @@ delete Object.prototype.__proto__;
|
||||||
/** @param {ts.Diagnostic[]} diagnostics */
|
/** @param {ts.Diagnostic[]} diagnostics */
|
||||||
function fromTypeScriptDiagnostic(diagnostics) {
|
function fromTypeScriptDiagnostic(diagnostics) {
|
||||||
return diagnostics.map(({ relatedInformation: ri, source, ...diag }) => {
|
return diagnostics.map(({ relatedInformation: ri, source, ...diag }) => {
|
||||||
|
/** @type {any} */
|
||||||
const value = fromRelatedInformation(diag);
|
const value = fromRelatedInformation(diag);
|
||||||
value.relatedInformation = ri
|
value.relatedInformation = ri
|
||||||
? ri.map(fromRelatedInformation)
|
? ri.map(fromRelatedInformation)
|
||||||
|
@ -106,7 +112,7 @@ delete Object.prototype.__proto__;
|
||||||
* Deno, as they provide misleading or incorrect information. */
|
* Deno, as they provide misleading or incorrect information. */
|
||||||
const IGNORED_DIAGNOSTICS = [
|
const IGNORED_DIAGNOSTICS = [
|
||||||
// TS1208: All files must be modules when the '--isolatedModules' flag is
|
// TS1208: All files must be modules when the '--isolatedModules' flag is
|
||||||
// provided. We can ignore because we guarantuee that all files are
|
// provided. We can ignore because we guarantee that all files are
|
||||||
// modules.
|
// modules.
|
||||||
1208,
|
1208,
|
||||||
// TS1375: 'await' expressions are only allowed at the top level of a file
|
// TS1375: 'await' expressions are only allowed at the top level of a file
|
||||||
|
@ -148,10 +154,72 @@ delete Object.prototype.__proto__;
|
||||||
target: ts.ScriptTarget.ESNext,
|
target: ts.ScriptTarget.ESNext,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class ScriptSnapshot {
|
||||||
|
/** @type {string} */
|
||||||
|
specifier;
|
||||||
|
/** @type {string} */
|
||||||
|
version;
|
||||||
|
/**
|
||||||
|
* @param {string} specifier
|
||||||
|
* @param {string} version
|
||||||
|
*/
|
||||||
|
constructor(specifier, version) {
|
||||||
|
this.specifier = specifier;
|
||||||
|
this.version = version;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {number} start
|
||||||
|
* @param {number} end
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getText(start, end) {
|
||||||
|
const { specifier, version } = this;
|
||||||
|
debug(
|
||||||
|
`snapshot.getText(${start}, ${end}) specifier: ${specifier} version: ${version}`,
|
||||||
|
);
|
||||||
|
return core.jsonOpSync("op_get_text", { specifier, version, start, end });
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
getLength() {
|
||||||
|
const { specifier, version } = this;
|
||||||
|
debug(`snapshot.getLength() specifier: ${specifier} version: ${version}`);
|
||||||
|
return core.jsonOpSync("op_get_length", { specifier, version });
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {ScriptSnapshot} oldSnapshot
|
||||||
|
* @returns {ts.TextChangeRange | undefined}
|
||||||
|
*/
|
||||||
|
getChangeRange(oldSnapshot) {
|
||||||
|
const { specifier, version } = this;
|
||||||
|
const { version: oldVersion } = oldSnapshot;
|
||||||
|
const oldLength = oldSnapshot.getLength();
|
||||||
|
debug(
|
||||||
|
`snapshot.getLength() specifier: ${specifier} oldVersion: ${oldVersion} version: ${version}`,
|
||||||
|
);
|
||||||
|
return core.jsonOpSync(
|
||||||
|
"op_get_change_range",
|
||||||
|
{ specifier, oldLength, oldVersion, version },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
dispose() {
|
||||||
|
const { specifier, version } = this;
|
||||||
|
debug(`snapshot.dispose() specifier: ${specifier} version: ${version}`);
|
||||||
|
core.jsonOpSync("op_dispose", { specifier, version });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {ts.CompilerOptions} */
|
||||||
|
let compilationSettings = {};
|
||||||
|
|
||||||
|
/** @type {ts.LanguageService} */
|
||||||
|
let languageService;
|
||||||
|
|
||||||
/** An object literal of the incremental compiler host, which provides the
|
/** An object literal of the incremental compiler host, which provides the
|
||||||
* specific "bindings" to the Deno environment that tsc needs to work.
|
* specific "bindings" to the Deno environment that tsc needs to work.
|
||||||
*
|
*
|
||||||
* @type {ts.CompilerHost} */
|
* @type {ts.CompilerHost & ts.LanguageServiceHost} */
|
||||||
const host = {
|
const host = {
|
||||||
fileExists(fileName) {
|
fileExists(fileName) {
|
||||||
debug(`host.fileExists("${fileName}")`);
|
debug(`host.fileExists("${fileName}")`);
|
||||||
|
@ -231,21 +299,73 @@ delete Object.prototype.__proto__;
|
||||||
debug(`host.resolveModuleNames()`);
|
debug(`host.resolveModuleNames()`);
|
||||||
debug(` base: ${base}`);
|
debug(` base: ${base}`);
|
||||||
debug(` specifiers: ${specifiers.join(", ")}`);
|
debug(` specifiers: ${specifiers.join(", ")}`);
|
||||||
/** @type {Array<[string, ts.Extension]>} */
|
/** @type {Array<[string, ts.Extension] | undefined>} */
|
||||||
const resolved = core.jsonOpSync("op_resolve", {
|
const resolved = core.jsonOpSync("op_resolve", {
|
||||||
specifiers,
|
specifiers,
|
||||||
base,
|
base,
|
||||||
});
|
});
|
||||||
const r = resolved.map(([resolvedFileName, extension]) => ({
|
if (resolved) {
|
||||||
resolvedFileName,
|
const result = resolved.map((item) => {
|
||||||
extension,
|
if (item) {
|
||||||
isExternalLibraryImport: false,
|
const [resolvedFileName, extension] = item;
|
||||||
}));
|
return {
|
||||||
return r;
|
resolvedFileName,
|
||||||
|
extension,
|
||||||
|
isExternalLibraryImport: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
result.length = specifiers.length;
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
return new Array(specifiers.length);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
createHash(data) {
|
createHash(data) {
|
||||||
return core.jsonOpSync("op_create_hash", { data }).hash;
|
return core.jsonOpSync("op_create_hash", { data }).hash;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// LanguageServiceHost
|
||||||
|
getCompilationSettings() {
|
||||||
|
debug("host.getCompilationSettings()");
|
||||||
|
return compilationSettings;
|
||||||
|
},
|
||||||
|
getScriptFileNames() {
|
||||||
|
debug("host.getScriptFileNames()");
|
||||||
|
return core.jsonOpSync("op_script_names", undefined);
|
||||||
|
},
|
||||||
|
getScriptVersion(specifier) {
|
||||||
|
debug(`host.getScriptVersion("${specifier}")`);
|
||||||
|
const sourceFile = sourceFileCache.get(specifier);
|
||||||
|
if (sourceFile) {
|
||||||
|
return sourceFile.version ?? "1";
|
||||||
|
}
|
||||||
|
return core.jsonOpSync("op_script_version", { specifier });
|
||||||
|
},
|
||||||
|
getScriptSnapshot(specifier) {
|
||||||
|
debug(`host.getScriptSnapshot("${specifier}")`);
|
||||||
|
const sourceFile = sourceFileCache.get(specifier);
|
||||||
|
if (sourceFile) {
|
||||||
|
return {
|
||||||
|
getText(start, end) {
|
||||||
|
return sourceFile.text.substring(start, end);
|
||||||
|
},
|
||||||
|
getLength() {
|
||||||
|
return sourceFile.text.length;
|
||||||
|
},
|
||||||
|
getChangeRange() {
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/** @type {string | undefined} */
|
||||||
|
const version = core.jsonOpSync("op_script_version", { specifier });
|
||||||
|
if (version != null) {
|
||||||
|
return new ScriptSnapshot(specifier, version);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @type {Array<[string, number]>} */
|
/** @type {Array<[string, number]>} */
|
||||||
|
@ -254,10 +374,13 @@ delete Object.prototype.__proto__;
|
||||||
|
|
||||||
function performanceStart() {
|
function performanceStart() {
|
||||||
stats.length = 0;
|
stats.length = 0;
|
||||||
statsStart = new Date();
|
statsStart = Date.now();
|
||||||
ts.performance.enable();
|
ts.performance.enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{ program: ts.Program | ts.EmitAndSemanticDiagnosticsBuilderProgram, fileCount?: number }} options
|
||||||
|
*/
|
||||||
function performanceProgram({ program, fileCount }) {
|
function performanceProgram({ program, fileCount }) {
|
||||||
if (program) {
|
if (program) {
|
||||||
if ("getProgram" in program) {
|
if ("getProgram" in program) {
|
||||||
|
@ -286,7 +409,7 @@ delete Object.prototype.__proto__;
|
||||||
}
|
}
|
||||||
|
|
||||||
function performanceEnd() {
|
function performanceEnd() {
|
||||||
const duration = new Date() - statsStart;
|
const duration = Date.now() - statsStart;
|
||||||
stats.push(["Compile time", duration]);
|
stats.push(["Compile time", duration]);
|
||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
@ -308,7 +431,7 @@ delete Object.prototype.__proto__;
|
||||||
debug(config);
|
debug(config);
|
||||||
|
|
||||||
const { options, errors: configFileParsingDiagnostics } = ts
|
const { options, errors: configFileParsingDiagnostics } = ts
|
||||||
.convertCompilerOptionsFromJson(config, "", "tsconfig.json");
|
.convertCompilerOptionsFromJson(config, "");
|
||||||
// The `allowNonTsExtensions` is a "hidden" compiler option used in VSCode
|
// The `allowNonTsExtensions` is a "hidden" compiler option used in VSCode
|
||||||
// which is not allowed to be passed in JSON, we need it to allow special
|
// which is not allowed to be passed in JSON, we need it to allow special
|
||||||
// URLs which Deno supports. So we need to either ignore the diagnostic, or
|
// URLs which Deno supports. So we need to either ignore the diagnostic, or
|
||||||
|
@ -340,6 +463,106 @@ delete Object.prototype.__proto__;
|
||||||
debug("<<< exec stop");
|
debug("<<< exec stop");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} id
|
||||||
|
* @param {any} data
|
||||||
|
*/
|
||||||
|
function respond(id, data = null) {
|
||||||
|
core.jsonOpSync("op_respond", { id, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {LanguageServerRequest} request
|
||||||
|
*/
|
||||||
|
function serverRequest({ id, ...request }) {
|
||||||
|
debug(`serverRequest()`, { id, ...request });
|
||||||
|
switch (request.method) {
|
||||||
|
case "configure": {
|
||||||
|
const { options, errors } = ts
|
||||||
|
.convertCompilerOptionsFromJson(request.compilerOptions, "");
|
||||||
|
Object.assign(options, { allowNonTsExtensions: true });
|
||||||
|
if (errors.length) {
|
||||||
|
debug(ts.formatDiagnostics(errors, host));
|
||||||
|
}
|
||||||
|
compilationSettings = options;
|
||||||
|
return respond(id, true);
|
||||||
|
}
|
||||||
|
case "getSemanticDiagnostics": {
|
||||||
|
const diagnostics = languageService.getSemanticDiagnostics(
|
||||||
|
request.specifier,
|
||||||
|
).filter(({ code }) => !IGNORED_DIAGNOSTICS.includes(code));
|
||||||
|
return respond(id, fromTypeScriptDiagnostic(diagnostics));
|
||||||
|
}
|
||||||
|
case "getSuggestionDiagnostics": {
|
||||||
|
const diagnostics = languageService.getSuggestionDiagnostics(
|
||||||
|
request.specifier,
|
||||||
|
).filter(({ code }) => !IGNORED_DIAGNOSTICS.includes(code));
|
||||||
|
return respond(id, fromTypeScriptDiagnostic(diagnostics));
|
||||||
|
}
|
||||||
|
case "getSyntacticDiagnostics": {
|
||||||
|
const diagnostics = languageService.getSyntacticDiagnostics(
|
||||||
|
request.specifier,
|
||||||
|
).filter(({ code }) => !IGNORED_DIAGNOSTICS.includes(code));
|
||||||
|
return respond(id, fromTypeScriptDiagnostic(diagnostics));
|
||||||
|
}
|
||||||
|
case "getQuickInfo": {
|
||||||
|
return respond(
|
||||||
|
id,
|
||||||
|
languageService.getQuickInfoAtPosition(
|
||||||
|
request.specifier,
|
||||||
|
request.position,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "getDocumentHighlights": {
|
||||||
|
return respond(
|
||||||
|
id,
|
||||||
|
languageService.getDocumentHighlights(
|
||||||
|
request.specifier,
|
||||||
|
request.position,
|
||||||
|
request.filesToSearch,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "getReferences": {
|
||||||
|
return respond(
|
||||||
|
id,
|
||||||
|
languageService.getReferencesAtPosition(
|
||||||
|
request.specifier,
|
||||||
|
request.position,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "getDefinition": {
|
||||||
|
return respond(
|
||||||
|
id,
|
||||||
|
languageService.getDefinitionAndBoundSpan(
|
||||||
|
request.specifier,
|
||||||
|
request.position,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new TypeError(
|
||||||
|
// @ts-ignore exhausted case statement sets type to never
|
||||||
|
`Invalid request method for request: "${request.method}" (${id})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {{ debug: boolean; }} init */
|
||||||
|
function serverInit({ debug: debugFlag }) {
|
||||||
|
if (hasStarted) {
|
||||||
|
throw new Error("The language server has already been initialized.");
|
||||||
|
}
|
||||||
|
hasStarted = true;
|
||||||
|
languageService = ts.createLanguageService(host);
|
||||||
|
core.ops();
|
||||||
|
core.registerErrorClass("Error", Error);
|
||||||
|
setLogDebug(debugFlag, "TSLS");
|
||||||
|
debug("serverInit()");
|
||||||
|
}
|
||||||
|
|
||||||
let hasStarted = false;
|
let hasStarted = false;
|
||||||
|
|
||||||
/** Startup the runtime environment, setting various flags.
|
/** Startup the runtime environment, setting various flags.
|
||||||
|
@ -391,4 +614,9 @@ delete Object.prototype.__proto__;
|
||||||
// checking TypeScript.
|
// checking TypeScript.
|
||||||
globalThis.startup = startup;
|
globalThis.startup = startup;
|
||||||
globalThis.exec = exec;
|
globalThis.exec = exec;
|
||||||
|
|
||||||
|
// exposes the functions that are called when the compiler is used as a
|
||||||
|
// language service.
|
||||||
|
globalThis.serverInit = serverInit;
|
||||||
|
globalThis.serverRequest = serverRequest;
|
||||||
})(this);
|
})(this);
|
||||||
|
|
103
cli/tsc/compiler.d.ts
vendored
Normal file
103
cli/tsc/compiler.d.ts
vendored
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
// Contains types that can be used to validate and check `99_main_compiler.js`
|
||||||
|
|
||||||
|
import * as _ts from "../dts/typescript";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// deno-lint-ignore no-namespace
|
||||||
|
namespace ts {
|
||||||
|
var libs: string[];
|
||||||
|
var libMap: Map<string, string>;
|
||||||
|
|
||||||
|
interface SourceFile {
|
||||||
|
version?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Performance {
|
||||||
|
enable(): void;
|
||||||
|
getDuration(value: string): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
var performance: Performance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// deno-lint-ignore no-namespace
|
||||||
|
namespace ts {
|
||||||
|
export = _ts;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Object {
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
__proto__: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DenoCore {
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
jsonOpSync<T>(name: string, params: T): any;
|
||||||
|
ops(): void;
|
||||||
|
print(msg: string): void;
|
||||||
|
registerErrorClass(name: string, Ctor: typeof Error): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type LanguageServerRequest =
|
||||||
|
| ConfigureRequest
|
||||||
|
| GetSyntacticDiagnosticsRequest
|
||||||
|
| GetSemanticDiagnosticsRequest
|
||||||
|
| GetSuggestionDiagnosticsRequest
|
||||||
|
| GetQuickInfoRequest
|
||||||
|
| GetDocumentHighlightsRequest
|
||||||
|
| GetReferencesRequest
|
||||||
|
| GetDefinitionRequest;
|
||||||
|
|
||||||
|
interface BaseLanguageServerRequest {
|
||||||
|
id: number;
|
||||||
|
method: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConfigureRequest extends BaseLanguageServerRequest {
|
||||||
|
method: "configure";
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
compilerOptions: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSyntacticDiagnosticsRequest extends BaseLanguageServerRequest {
|
||||||
|
method: "getSyntacticDiagnostics";
|
||||||
|
specifier: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSemanticDiagnosticsRequest extends BaseLanguageServerRequest {
|
||||||
|
method: "getSemanticDiagnostics";
|
||||||
|
specifier: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSuggestionDiagnosticsRequest extends BaseLanguageServerRequest {
|
||||||
|
method: "getSuggestionDiagnostics";
|
||||||
|
specifier: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetQuickInfoRequest extends BaseLanguageServerRequest {
|
||||||
|
method: "getQuickInfo";
|
||||||
|
specifier: string;
|
||||||
|
position: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetDocumentHighlightsRequest extends BaseLanguageServerRequest {
|
||||||
|
method: "getDocumentHighlights";
|
||||||
|
specifier: string;
|
||||||
|
position: number;
|
||||||
|
filesToSearch: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetReferencesRequest extends BaseLanguageServerRequest {
|
||||||
|
method: "getReferences";
|
||||||
|
specifier: string;
|
||||||
|
position: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetDefinitionRequest extends BaseLanguageServerRequest {
|
||||||
|
method: "getDefinition";
|
||||||
|
specifier: string;
|
||||||
|
position: number;
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,7 +52,7 @@ impl fmt::Display for IgnoredCompilerOptions {
|
||||||
/// A static slice of all the compiler options that should be ignored that
|
/// A static slice of all the compiler options that should be ignored that
|
||||||
/// either have no effect on the compilation or would cause the emit to not work
|
/// either have no effect on the compilation or would cause the emit to not work
|
||||||
/// in Deno.
|
/// in Deno.
|
||||||
const IGNORED_COMPILER_OPTIONS: &[&str] = &[
|
pub const IGNORED_COMPILER_OPTIONS: &[&str] = &[
|
||||||
"allowSyntheticDefaultImports",
|
"allowSyntheticDefaultImports",
|
||||||
"allowUmdGlobalAccess",
|
"allowUmdGlobalAccess",
|
||||||
"baseUrl",
|
"baseUrl",
|
||||||
|
@ -83,7 +83,7 @@ const IGNORED_COMPILER_OPTIONS: &[&str] = &[
|
||||||
"useDefineForClassFields",
|
"useDefineForClassFields",
|
||||||
];
|
];
|
||||||
|
|
||||||
const IGNORED_RUNTIME_COMPILER_OPTIONS: &[&str] = &[
|
pub const IGNORED_RUNTIME_COMPILER_OPTIONS: &[&str] = &[
|
||||||
"assumeChangesOnlyAffectDirectDependencies",
|
"assumeChangesOnlyAffectDirectDependencies",
|
||||||
"build",
|
"build",
|
||||||
"charset",
|
"charset",
|
||||||
|
|
Loading…
Reference in a new issue