summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKitson Kelly <me@kitsonkelly.com>2020-12-07 21:46:39 +1100
committerGitHub <noreply@github.com>2020-12-07 21:46:39 +1100
commit301d3e4b6849d24154ac2d65c00a9b30223d000e (patch)
treeab3bc074493e6c9be8d1875233bc141bdc0da3b4
parentc8e9b2654ec0d54c77bb3f49fa31c3986203d517 (diff)
feat: add mvp language server (#8515)
Resolves #8400
-rw-r--r--Cargo.lock66
-rw-r--r--cli/Cargo.toml22
-rw-r--r--cli/ast.rs2
-rw-r--r--cli/file_fetcher.rs4
-rw-r--r--cli/flags.rs38
-rw-r--r--cli/http_cache.rs2
-rw-r--r--cli/lsp/README.md23
-rw-r--r--cli/lsp/analysis.rs324
-rw-r--r--cli/lsp/capabilities.rs59
-rw-r--r--cli/lsp/config.rs49
-rw-r--r--cli/lsp/diagnostics.rs268
-rw-r--r--cli/lsp/dispatch.rs185
-rw-r--r--cli/lsp/handlers.rs266
-rw-r--r--cli/lsp/lsp_extensions.rs26
-rw-r--r--cli/lsp/memory_cache.rs126
-rw-r--r--cli/lsp/mod.rs415
-rw-r--r--cli/lsp/sources.rs372
-rw-r--r--cli/lsp/state.rs292
-rw-r--r--cli/lsp/text.rs514
-rw-r--r--cli/lsp/tsc.rs1210
-rw-r--r--cli/lsp/utils.rs114
-rw-r--r--cli/main.rs6
-rw-r--r--cli/module_graph.rs14
-rw-r--r--cli/tests/integration_tests.rs2
-rw-r--r--cli/tests/lsp/did_open_notification.json12
-rw-r--r--cli/tests/lsp/exit_notification.json5
-rw-r--r--cli/tests/lsp/hover_request.json14
-rw-r--r--cli/tests/lsp/initialize_request.json23
-rw-r--r--cli/tests/lsp/initialized_notification.json5
-rw-r--r--cli/tests/lsp/shutdown_request.json6
-rw-r--r--cli/tests/lsp_tests.rs88
-rw-r--r--cli/tests/type_directives_01.ts.out2
-rw-r--r--cli/tests/type_directives_02.ts.out2
-rw-r--r--cli/tools/lint.rs2
-rw-r--r--cli/tsc.rs6
-rw-r--r--cli/tsc/99_main_compiler.js254
-rw-r--r--cli/tsc/compiler.d.ts103
-rw-r--r--cli/tsc_config.rs4
38 files changed, 4878 insertions, 47 deletions
diff --git a/Cargo.lock b/Cargo.lock
index a34c10ed9..224d0f322 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -366,11 +366,21 @@ version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87"
dependencies = [
- "crossbeam-utils",
+ "crossbeam-utils 0.7.2",
"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]]
name = "crossbeam-utils"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -382,6 +392,17 @@ dependencies = [
]
[[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]]
name = "darling"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -437,6 +458,7 @@ dependencies = [
"bytes 0.5.6",
"chrono",
"clap",
+ "crossbeam-channel 0.5.0",
"deno_core",
"deno_crypto",
"deno_doc",
@@ -457,9 +479,12 @@ dependencies = [
"lazy_static",
"libc",
"log",
+ "lsp-server",
+ "lsp-types",
"nix",
"notify",
"os_pipe",
+ "percent-encoding",
"regex",
"ring",
"rustyline",
@@ -1315,6 +1340,32 @@ dependencies = [
]
[[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]]
name = "matches"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1513,7 +1564,7 @@ checksum = "77d03607cf88b4b160ba0e9ed425fff3cee3b55ac813f0c685b3a3772da37d0e"
dependencies = [
"anymap",
"bitflags",
- "crossbeam-channel",
+ "crossbeam-channel 0.4.4",
"filetime",
"fsevent",
"fsevent-sys",
@@ -2283,6 +2334,17 @@ dependencies = [
]
[[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]]
name = "serde_urlencoded"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index a8d6e9c2b..95ffac7fe 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -32,22 +32,24 @@ winres = "0.1.11"
winapi = "0.3.9"
[dependencies]
-deno_crypto = { path = "../op_crates/crypto", version = "0.3.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_fetch = { path = "../op_crates/fetch", version = "0.12.0" }
deno_lint = "0.2.12"
deno_web = { path = "../op_crates/web", version = "0.20.0" }
-deno_fetch = { path = "../op_crates/fetch", version = "0.12.0" }
atty = "0.2.14"
base64 = "0.12.3"
bytes = "0.5.6"
byteorder = "1.3.4"
clap = "2.33.3"
+crossbeam-channel = "0.5.0"
dissimilar = "1.0.2"
dlopen = "0.1.8"
-encoding_rs = "0.8.24"
dprint-plugin-typescript = "0.35.0"
+encoding_rs = "0.8.24"
+env_logger = "0.7.1"
filetime = "0.2.12"
http = "0.2.1"
indexmap = "1.6.0"
@@ -55,31 +57,33 @@ jsonc-parser = "0.14.0"
lazy_static = "1.4.0"
libc = "0.2.77"
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"
+percent-encoding = "2.1.0"
regex = "1.3.9"
ring = "0.16.15"
rustyline = { version = "7.0.0", default-features = false }
rustyline-derive = "0.4.0"
+semver-parser = "0.9.0"
serde = { version = "1.0.116", features = ["derive"] }
shell-escape = "0.1.5"
-sys-info = "0.7.0"
sourcemap = "6.0.1"
swc_bundler = "0.17.5"
swc_common = { version = "0.10.6", features = ["sourcemap"] }
swc_ecmascript = { version = "0.14.4", features = ["codegen", "dep_graph", "parser", "react", "transforms", "visit"] }
+sys-info = "0.7.0"
tempfile = "3.1.0"
termcolor = "1.1.0"
tokio = { version = "0.2.22", features = ["full"] }
tokio-rustls = "0.14.1"
# Keep in-sync with warp.
tokio-tungstenite = "0.11.0"
-webpki = "0.21.3"
-webpki-roots = "=0.19.0" # Pinned to v0.19.0 to match 'reqwest'.
+uuid = { version = "0.8.1", features = ["v4"] }
walkdir = "2.3.1"
warp = { version = "0.2.5", features = ["tls"] }
-semver-parser = "0.9.0"
-uuid = { version = "0.8.1", features = ["v4"] }
+webpki = "0.21.3"
+webpki-roots = "=0.19.0" # Pinned to v0.19.0 to match 'reqwest'.
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.9", features = ["knownfolders", "mswsock", "objbase", "shlobj", "tlhelp32", "winbase", "winerror", "winsock2"] }
diff --git a/cli/ast.rs b/cli/ast.rs
index 10d7b5383..ef6468310 100644
--- a/cli/ast.rs
+++ b/cli/ast.rs
@@ -354,7 +354,7 @@ impl ParsedModule {
}
}
-fn parse_with_source_map(
+pub fn parse_with_source_map(
specifier: &str,
source: &str,
media_type: &MediaType,
diff --git a/cli/file_fetcher.rs b/cli/file_fetcher.rs
index 0d11852d1..5b2f6f74c 100644
--- a/cli/file_fetcher.rs
+++ b/cli/file_fetcher.rs
@@ -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
/// string.
-fn get_source_from_bytes(
+pub fn get_source_from_bytes(
bytes: Vec<u8>,
maybe_charset: Option<String>,
) -> Result<String, AnyError> {
@@ -161,7 +161,7 @@ fn get_validated_scheme(
/// Resolve a media type and optionally the charset from a module specifier and
/// the value of a content type header.
-fn map_content_type(
+pub fn map_content_type(
specifier: &ModuleSpecifier,
maybe_content_type: Option<String>,
) -> (MediaType, Option<String>) {
diff --git a/cli/flags.rs b/cli/flags.rs
index 5ff21971d..2210d7565 100644
--- a/cli/flags.rs
+++ b/cli/flags.rs
@@ -17,6 +17,9 @@ pub enum DenoSubcommand {
source_file: String,
out_file: Option<PathBuf>,
},
+ Cache {
+ files: Vec<String>,
+ },
Compile {
source_file: String,
output: Option<PathBuf>,
@@ -35,9 +38,6 @@ pub enum DenoSubcommand {
code: String,
as_typescript: bool,
},
- Cache {
- files: Vec<String>,
- },
Fmt {
check: bool,
files: Vec<PathBuf>,
@@ -54,6 +54,7 @@ pub enum DenoSubcommand {
root: Option<PathBuf>,
force: bool,
},
+ LanguageServer,
Lint {
files: 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);
} else if let Some(m) = matches.subcommand_matches("compile") {
compile_parse(&mut flags, m);
+ } else if let Some(m) = matches.subcommand_matches("lsp") {
+ language_server_parse(&mut flags, m);
} else {
repl_parse(&mut flags, &matches);
}
@@ -349,6 +352,7 @@ If the flag is set, restrict these messages to errors.",
.subcommand(fmt_subcommand())
.subcommand(info_subcommand())
.subcommand(install_subcommand())
+ .subcommand(language_server_subcommand())
.subcommand(lint_subcommand())
.subcommand(repl_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) {
let files = match matches.values_of("files") {
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> {
SubCommand::with_name("lint")
.about("Lint source files")
@@ -1953,6 +1973,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]
fn lint() {
let r = flags_from_vec_safe(svec![
"deno",
diff --git a/cli/http_cache.rs b/cli/http_cache.rs
index 9cf2adc1a..dd5f4dc3f 100644
--- a/cli/http_cache.rs
+++ b/cli/http_cache.rs
@@ -72,7 +72,7 @@ pub fn url_to_filename(url: &Url) -> PathBuf {
cache_filename
}
-#[derive(Clone)]
+#[derive(Debug, Clone, Default)]
pub struct HttpCache {
pub location: PathBuf,
}
diff --git a/cli/lsp/README.md b/cli/lsp/README.md
new file mode 100644
index 000000000..dcc953273
--- /dev/null
+++ b/cli/lsp/README.md
@@ -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.
diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs
new file mode 100644
index 000000000..370b41c45
--- /dev/null
+++ b/cli/lsp/analysis.rs
@@ -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,
+ })
+ );
+ }
+}
diff --git a/cli/lsp/capabilities.rs b/cli/lsp/capabilities.rs
new file mode 100644
index 000000000..cf8f150ca
--- /dev/null
+++ b/cli/lsp/capabilities.rs
@@ -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,
+ }
+}
diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs
new file mode 100644
index 000000000..ebc145708
--- /dev/null
+++ b/cli/lsp/config.rs
@@ -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");
+ }
+ }
+}
diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs
new file mode 100644
index 000000000..a7f027c1b
--- /dev/null
+++ b/cli/lsp/diagnostics.rs
@@ -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)
+}
diff --git a/cli/lsp/dispatch.rs b/cli/lsp/dispatch.rs
new file mode 100644
index 000000000..774bdcef9
--- /dev/null
+++ b/cli/lsp/dispatch.rs
@@ -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
+ }
+ }
+ }
+}
diff --git a/cli/lsp/handlers.rs b/cli/lsp/handlers.rs
new file mode 100644
index 000000000..6dd7321c7
--- /dev/null
+++ b/cli/lsp/handlers.rs
@@ -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)
+}
diff --git a/cli/lsp/lsp_extensions.rs b/cli/lsp/lsp_extensions.rs
new file mode 100644
index 000000000..eb0a62464
--- /dev/null
+++ b/cli/lsp/lsp_extensions.rs
@@ -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";
+}
diff --git a/cli/lsp/memory_cache.rs b/cli/lsp/memory_cache.rs
new file mode 100644
index 000000000..75c5bdb25
--- /dev/null
+++ b/cli/lsp/memory_cache.rs
@@ -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()
+ }
+}
diff --git a/cli/lsp/mod.rs b/cli/lsp/mod.rs
new file mode 100644
index 000000000..c26c5d89e
--- /dev/null
+++ b/cli/lsp/mod.rs
@@ -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,
+ &params.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.",
+ ))
+ }
+}
diff --git a/cli/lsp/sources.rs b/cli/lsp/sources.rs
new file mode 100644
index 000000000..4f80044a2
--- /dev/null
+++ b/cli/lsp/sources.rs
@@ -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);
+ }
+}
diff --git a/cli/lsp/state.rs b/cli/lsp/state.rs
new file mode 100644
index 000000000..18a1e4023
--- /dev/null
+++ b/cli/lsp/state.rs
@@ -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", &notification.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;
+ }
+}
diff --git a/cli/lsp/text.rs b/cli/lsp/text.rs
new file mode 100644
index 000000000..5bca534c1
--- /dev/null
+++ b/cli/lsp/text.rs
@@ -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");
+ }
+}
diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs
new file mode 100644
index 000000000..65f6ebbdb
--- /dev/null
+++ b/cli/lsp/tsc.rs
@@ -0,0 +1,1210 @@
+// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
+
+use super::analysis::ResolvedImport;
+use super::state::ServerStateSnapshot;
+use super::text;
+use super::utils;
+
+use crate::js;
+use crate::media_type::MediaType;
+use crate::tsc::ResolveArgs;
+use crate::tsc_config::TsConfig;
+
+use deno_core::error::custom_error;
+use deno_core::error::AnyError;
+use deno_core::json_op_sync;
+use deno_core::serde::Deserialize;
+use deno_core::serde_json;
+use deno_core::serde_json::json;
+use deno_core::serde_json::Value;
+use deno_core::JsRuntime;
+use deno_core::ModuleSpecifier;
+use deno_core::OpFn;
+use deno_core::RuntimeOptions;
+use regex::Captures;
+use regex::Regex;
+use std::borrow::Cow;
+use std::collections::HashMap;
+
+/// Provide static assets for the language server.
+///
+/// TODO(@kitsonk) this should be DRY'ed up with `cli/tsc.rs` and the
+/// `cli/build.rs`
+pub fn get_asset(asset: &str) -> Option<&'static str> {
+ macro_rules! inc {
+ ($e:expr) => {
+ Some(include_str!(concat!("../dts/", $e)))
+ };
+ }
+ match asset {
+ // These are not included in the snapshot
+ "/lib.dom.d.ts" => inc!("lib.dom.d.ts"),
+ "/lib.dom.iterable.d.ts" => inc!("lib.dom.iterable.d.ts"),
+ "/lib.es6.d.ts" => inc!("lib.es6.d.ts"),
+ "/lib.es2016.full.d.ts" => inc!("lib.es2016.full.d.ts"),
+ "/lib.es2017.full.d.ts" => inc!("lib.es2017.full.d.ts"),
+ "/lib.es2018.full.d.ts" => inc!("lib.es2018.full.d.ts"),
+ "/lib.es2019.full.d.ts" => inc!("lib.es2019.full.d.ts"),
+ "/lib.es2020.full.d.ts" => inc!("lib.es2020.full.d.ts"),
+ "/lib.esnext.full.d.ts" => inc!("lib.esnext.full.d.ts"),
+ "/lib.scripthost.d.ts" => inc!("lib.scripthost.d.ts"),
+ "/lib.webworker.d.ts" => inc!("lib.webworker.d.ts"),
+ "/lib.webworker.importscripts.d.ts" => {
+ inc!("lib.webworker.importscripts.d.ts")
+ }
+ "/lib.webworker.iterable.d.ts" => inc!("lib.webworker.iterable.d.ts"),
+ // These come from op crates
+ // TODO(@kitsonk) these is even hackier than the rest of this...
+ "/lib.deno.web.d.ts" => {
+ Some(include_str!("../../op_crates/web/lib.deno_web.d.ts"))
+ }
+ "/lib.deno.fetch.d.ts" => {
+ Some(include_str!("../../op_crates/fetch/lib.deno_fetch.d.ts"))
+ }
+ // These are included in the snapshot for TypeScript, and could be retrieved
+ // from there?
+ "/lib.d.ts" => inc!("lib.d.ts"),
+ "/lib.deno.ns.d.ts" => inc!("lib.deno.ns.d.ts"),
+ "/lib.deno.shared_globals.d.ts" => inc!("lib.deno.shared_globals.d.ts"),
+ "/lib.deno.unstable.d.ts" => inc!("lib.deno.unstable.d.ts"),
+ "/lib.deno.window.d.ts" => inc!("lib.deno.window.d.ts"),
+ "/lib.deno.worker.d.ts" => inc!("lib.deno.worker.d.ts"),
+ "/lib.es5.d.ts" => inc!("lib.es5.d.ts"),
+ "/lib.es2015.collection.d.ts" => inc!("lib.es2015.collection.d.ts"),
+ "/lib.es2015.core.d.ts" => inc!("lib.es2015.core.d.ts"),
+ "/lib.es2015.d.ts" => inc!("lib.es2015.d.ts"),
+ "/lib.es2015.generator.d.ts" => inc!("lib.es2015.generator.d.ts"),
+ "/lib.es2015.iterable.d.ts" => inc!("lib.es2015.iterable.d.ts"),
+ "/lib.es2015.promise.d.ts" => inc!("lib.es2015.promise.d.ts"),
+ "/lib.es2015.proxy.d.ts" => inc!("lib.es2015.proxy.d.ts"),
+ "/lib.es2015.reflect.d.ts" => inc!("lib.es2015.reflect.d.ts"),
+ "/lib.es2015.symbol.d.ts" => inc!("lib.es2015.symbol.d.ts"),
+ "/lib.es2015.symbol.wellknown.d.ts" => {
+ inc!("lib.es2015.symbol.wellknown.d.ts")
+ }
+ "/lib.es2016.array.include.d.ts" => inc!("lib.es2016.array.include.d.ts"),
+ "/lib.es2016.d.ts" => inc!("lib.es2016.d.ts"),
+ "/lib.es2017.d.ts" => inc!("lib.es2017.d.ts"),
+ "/lib.es2017.intl.d.ts" => inc!("lib.es2017.intl.d.ts"),
+ "/lib.es2017.object.d.ts" => inc!("lib.es2017.object.d.ts"),
+ "/lib.es2017.sharedmemory.d.ts" => inc!("lib.es2017.sharedmemory.d.ts"),
+ "/lib.es2017.string.d.ts" => inc!("lib.es2017.string.d.ts"),
+ "/lib.es2017.typedarrays.d.ts" => inc!("lib.es2017.typedarrays.d.ts"),
+ "/lib.es2018.asyncgenerator.d.ts" => inc!("lib.es2018.asyncgenerator.d.ts"),
+ "/lib.es2018.asynciterable.d.ts" => inc!("lib.es2018.asynciterable.d.ts"),
+ "/lib.es2018.d.ts" => inc!("lib.es2018.d.ts"),
+ "/lib.es2018.intl.d.ts" => inc!("lib.es2018.intl.d.ts"),
+ "/lib.es2018.promise.d.ts" => inc!("lib.es2018.promise.d.ts"),
+ "/lib.es2018.regexp.d.ts" => inc!("lib.es2018.regexp.d.ts"),
+ "/lib.es2019.array.d.ts" => inc!("lib.es2019.array.d.ts"),
+ "/lib.es2019.d.ts" => inc!("lib.es2019.d.ts"),
+ "/lib.es2019.object.d.ts" => inc!("lib.es2019.object.d.ts"),
+ "/lib.es2019.string.d.ts" => inc!("lib.es2019.string.d.ts"),
+ "/lib.es2019.symbol.d.ts" => inc!("lib.es2019.symbol.d.ts"),
+ "/lib.es2020.bigint.d.ts" => inc!("lib.es2020.bigint.d.ts"),
+ "/lib.es2020.d.ts" => inc!("lib.es2020.d.ts"),
+ "/lib.es2020.intl.d.ts" => inc!("lib.es2020.intl.d.ts"),
+ "/lib.es2020.promise.d.ts" => inc!("lib.es2020.promise.d.ts"),
+ "/lib.es2020.sharedmemory.d.ts" => inc!("lib.es2020.sharedmemory.d.ts"),
+ "/lib.es2020.string.d.ts" => inc!("lib.es2020.string.d.ts"),
+ "/lib.es2020.symbol.wellknown.d.ts" => {
+ inc!("lib.es2020.symbol.wellknown.d.ts")
+ }
+ "/lib.esnext.d.ts" => inc!("lib.esnext.d.ts"),
+ "/lib.esnext.intl.d.ts" => inc!("lib.esnext.intl.d.ts"),
+ "/lib.esnext.promise.d.ts" => inc!("lib.esnext.promise.d.ts"),
+ "/lib.esnext.string.d.ts" => inc!("lib.esnext.string.d.ts"),
+ "/lib.esnext.weakref.d.ts" => inc!("lib.esnext.weakref.d.ts"),
+ _ => None,
+ }
+}
+
+fn display_parts_to_string(
+ maybe_parts: Option<Vec<SymbolDisplayPart>>,
+) -> Option<String> {
+ maybe_parts.map(|parts| {
+ parts
+ .into_iter()
+ .map(|p| p.text)
+ .collect::<Vec<String>>()
+ .join("")
+ })
+}
+
+fn get_tag_body_text(tag: &JSDocTagInfo) -> Option<String> {
+ tag.text.as_ref().map(|text| match tag.name.as_str() {
+ "example" => {
+ let caption_regex =
+ Regex::new(r"<caption>(.*?)</caption>\s*\r?\n((?:\s|\S)*)").unwrap();
+ if caption_regex.is_match(&text) {
+ caption_regex
+ .replace(text, |c: &Captures| {
+ format!("{}\n\n{}", &c[1], make_codeblock(&c[2]))
+ })
+ .to_string()
+ } else {
+ make_codeblock(text)
+ }
+ }
+ "author" => {
+ let email_match_regex = Regex::new(r"(.+)\s<([-.\w]+@[-.\w]+)>").unwrap();
+ email_match_regex
+ .replace(text, |c: &Captures| format!("{} {}", &c[1], &c[2]))
+ .to_string()
+ }
+ "default" => make_codeblock(text),
+ _ => replace_links(text),
+ })
+}
+
+fn get_tag_documentation(tag: &JSDocTagInfo) -> String {
+ match tag.name.as_str() {
+ "augments" | "extends" | "param" | "template" => {
+ if let Some(text) = &tag.text {
+ let part_regex = Regex::new(r"^(\S+)\s*-?\s*").unwrap();
+ let body: Vec<&str> = part_regex.split(&text).collect();
+ if body.len() == 3 {
+ let param = body[1];
+ let doc = body[2];
+ let label = format!("*@{}* `{}`", tag.name, param);
+ if doc.is_empty() {
+ return label;
+ }
+ if doc.contains('\n') {
+ return format!("{} \n{}", label, replace_links(doc));
+ } else {
+ return format!("{} - {}", label, replace_links(doc));
+ }
+ }
+ }
+ }
+ _ => (),
+ }
+ let label = format!("*@{}*", tag.name);
+ let maybe_text = get_tag_body_text(tag);
+ if let Some(text) = maybe_text {
+ if text.contains('\n') {
+ format!("{} \n{}", label, text)
+ } else {
+ format!("{} - {}", label, text)
+ }
+ } else {
+ label
+ }
+}
+
+fn make_codeblock(text: &str) -> String {
+ let codeblock_regex = Regex::new(r"^\s*[~`]{3}").unwrap();
+ if codeblock_regex.is_match(text) {
+ text.to_string()
+ } else {
+ format!("```\n{}\n```", text)
+ }
+}
+
+/// Replace JSDoc like links (`{@link http://example.com}`) with markdown links
+fn replace_links(text: &str) -> String {
+ let jsdoc_links_regex = Regex::new(r"(?i)\{@(link|linkplain|linkcode) (https?://[^ |}]+?)(?:[| ]([^{}\n]+?))?\}").unwrap();
+ jsdoc_links_regex
+ .replace_all(text, |c: &Captures| match &c[1] {
+ "linkcode" => format!(
+ "[`{}`]({})",
+ if c.get(3).is_none() {
+ &c[2]
+ } else {
+ c[3].trim()
+ },
+ &c[2]
+ ),
+ _ => format!(
+ "[{}]({})",
+ if c.get(3).is_none() {
+ &c[2]
+ } else {
+ c[3].trim()
+ },
+ &c[2]
+ ),
+ })
+ .to_string()
+}
+
+#[derive(Debug, Deserialize)]
+pub enum ScriptElementKind {
+ #[serde(rename = "")]
+ Unknown,
+ #[serde(rename = "warning")]
+ Warning,
+ #[serde(rename = "keyword")]
+ Keyword,
+ #[serde(rename = "script")]
+ ScriptElement,
+ #[serde(rename = "module")]
+ ModuleElement,
+ #[serde(rename = "class")]
+ ClassElement,
+ #[serde(rename = "local class")]
+ LocalClassElement,
+ #[serde(rename = "interface")]
+ InterfaceElement,
+ #[serde(rename = "type")]
+ TypeElement,
+ #[serde(rename = "enum")]
+ EnumElement,
+ #[serde(rename = "enum member")]
+ EnumMemberElement,
+ #[serde(rename = "var")]
+ VariableElement,
+ #[serde(rename = "local var")]
+ LocalVariableElement,
+ #[serde(rename = "function")]
+ FunctionElement,
+ #[serde(rename = "local function")]
+ LocalFunctionElement,
+ #[serde(rename = "method")]
+ MemberFunctionElement,
+ #[serde(rename = "getter")]
+ MemberGetAccessorElement,
+ #[serde(rename = "setter")]
+ MemberSetAccessorElement,
+ #[serde(rename = "property")]
+ MemberVariableElement,
+ #[serde(rename = "constructor")]
+ ConstructorImplementationElement,
+ #[serde(rename = "call")]
+ CallSignatureElement,
+ #[serde(rename = "index")]
+ IndexSignatureElement,
+ #[serde(rename = "construct")]
+ ConstructSignatureElement,
+ #[serde(rename = "parameter")]
+ ParameterElement,
+ #[serde(rename = "type parameter")]
+ TypeParameterElement,
+ #[serde(rename = "primitive type")]
+ PrimitiveType,
+ #[serde(rename = "label")]
+ Label,
+ #[serde(rename = "alias")]
+ Alias,
+ #[serde(rename = "const")]
+ ConstElement,
+ #[serde(rename = "let")]
+ LetElement,
+ #[serde(rename = "directory")]
+ Directory,
+ #[serde(rename = "external module name")]
+ ExternalModuleName,
+ #[serde(rename = "JSX attribute")]
+ JsxAttribute,
+ #[serde(rename = "string")]
+ String,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct TextSpan {
+ start: u32,
+ length: u32,
+}
+
+impl TextSpan {
+ pub fn to_range(&self, line_index: &[u32]) -> lsp_types::Range {
+ lsp_types::Range {
+ start: text::to_position(line_index, self.start),
+ end: text::to_position(line_index, self.start + self.length),
+ }
+ }
+}
+
+#[derive(Debug, Deserialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct SymbolDisplayPart {
+ text: String,
+ kind: String,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct JSDocTagInfo {
+ name: String,
+ text: Option<String>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct QuickInfo {
+ kind: ScriptElementKind,
+ kind_modifiers: String,
+ text_span: TextSpan,
+ display_parts: Option<Vec<SymbolDisplayPart>>,
+ documentation: Option<Vec<SymbolDisplayPart>>,
+ tags: Option<Vec<JSDocTagInfo>>,
+}
+
+impl QuickInfo {
+ pub fn to_hover(&self, line_index: &[u32]) -> lsp_types::Hover {
+ let mut contents = Vec::<lsp_types::MarkedString>::new();
+ if let Some(display_string) =
+ display_parts_to_string(self.display_parts.clone())
+ {
+ contents.push(lsp_types::MarkedString::from_language_code(
+ "typescript".to_string(),
+ display_string,
+ ));
+ }
+ if let Some(documentation) =
+ display_parts_to_string(self.documentation.clone())
+ {
+ contents.push(lsp_types::MarkedString::from_markdown(documentation));
+ }
+ if let Some(tags) = &self.tags {
+ let tags_preview = tags
+ .iter()
+ .map(get_tag_documentation)
+ .collect::<Vec<String>>()
+ .join(" \n\n");
+ if !tags_preview.is_empty() {
+ contents.push(lsp_types::MarkedString::from_markdown(format!(
+ "\n\n{}",
+ tags_preview
+ )));
+ }
+ }
+ lsp_types::Hover {
+ contents: lsp_types::HoverContents::Array(contents),
+ range: Some(self.text_span.to_range(line_index)),
+ }
+ }
+}
+
+#[derive(Debug, Deserialize)]
+pub enum HighlightSpanKind {
+ #[serde(rename = "none")]
+ None,
+ #[serde(rename = "definition")]
+ Definition,
+ #[serde(rename = "reference")]
+ Reference,
+ #[serde(rename = "writtenReference")]
+ WrittenReference,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct HighlightSpan {
+ file_name: Option<String>,
+ is_in_string: Option<bool>,
+ text_span: TextSpan,
+ context_span: Option<TextSpan>,
+ kind: HighlightSpanKind,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct DefinitionInfo {
+ kind: ScriptElementKind,
+ name: String,
+ container_kind: Option<ScriptElementKind>,
+ container_name: Option<String>,
+ text_span: TextSpan,
+ pub file_name: String,
+ original_text_span: Option<TextSpan>,
+ original_file_name: Option<String>,
+ context_span: Option<TextSpan>,
+ original_context_span: Option<TextSpan>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct DefinitionInfoAndBoundSpan {
+ pub definitions: Option<Vec<DefinitionInfo>>,
+ text_span: TextSpan,
+}
+
+impl DefinitionInfoAndBoundSpan {
+ pub fn to_definition<F>(
+ &self,
+ line_index: &[u32],
+ mut index_provider: F,
+ ) -> Option<lsp_types::GotoDefinitionResponse>
+ where
+ F: FnMut(ModuleSpecifier) -> Vec<u32>,
+ {
+ if let Some(definitions) = &self.definitions {
+ let location_links = definitions
+ .iter()
+ .map(|di| {
+ let target_specifier =
+ ModuleSpecifier::resolve_url(&di.file_name).unwrap();
+ let target_line_index = index_provider(target_specifier);
+ let target_uri = utils::normalize_file_name(&di.file_name).unwrap();
+ let (target_range, target_selection_range) =
+ if let Some(context_span) = &di.context_span {
+ (
+ context_span.to_range(&target_line_index),
+ di.text_span.to_range(&target_line_index),
+ )
+ } else {
+ (
+ di.text_span.to_range(&target_line_index),
+ di.text_span.to_range(&target_line_index),
+ )
+ };
+ lsp_types::LocationLink {
+ origin_selection_range: Some(self.text_span.to_range(line_index)),
+ target_uri,
+ target_range,
+ target_selection_range,
+ }
+ })
+ .collect();
+
+ Some(lsp_types::GotoDefinitionResponse::Link(location_links))
+ } else {
+ None
+ }
+ }
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct DocumentHighlights {
+ file_name: String,
+ highlight_spans: Vec<HighlightSpan>,
+}
+
+impl DocumentHighlights {
+ pub fn to_highlight(
+ &self,
+ line_index: &[u32],
+ ) -> Vec<lsp_types::DocumentHighlight> {
+ self
+ .highlight_spans
+ .iter()
+ .map(|hs| lsp_types::DocumentHighlight {
+ range: hs.text_span.to_range(line_index),
+ kind: match hs.kind {
+ HighlightSpanKind::WrittenReference => {
+ Some(lsp_types::DocumentHighlightKind::Write)
+ }
+ _ => Some(lsp_types::DocumentHighlightKind::Read),
+ },
+ })
+ .collect()
+ }
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ReferenceEntry {
+ is_write_access: bool,
+ pub is_definition: bool,
+ is_in_string: Option<bool>,
+ text_span: TextSpan,
+ pub file_name: String,
+ original_text_span: Option<TextSpan>,
+ original_file_name: Option<String>,
+ context_span: Option<TextSpan>,
+ original_context_span: Option<TextSpan>,
+}
+
+impl ReferenceEntry {
+ pub fn to_location(&self, line_index: &[u32]) -> lsp_types::Location {
+ let uri = utils::normalize_file_name(&self.file_name).unwrap();
+ lsp_types::Location {
+ uri,
+ range: self.text_span.to_range(line_index),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Deserialize)]
+struct Response {
+ id: usize,
+ data: Value,
+}
+
+struct State<'a> {
+ last_id: usize,
+ response: Option<Response>,
+ server_state: ServerStateSnapshot,
+ snapshots: HashMap<(Cow<'a, str>, Cow<'a, str>), String>,
+}
+
+impl<'a> State<'a> {
+ fn new(server_state: ServerStateSnapshot) -> Self {
+ Self {
+ last_id: 1,
+ response: None,
+ server_state,
+ snapshots: Default::default(),
+ }
+ }
+}
+
+/// If a snapshot is missing from the state cache, add it.
+fn cache_snapshot(
+ state: &mut State,
+ specifier: String,
+ version: String,
+) -> Result<(), AnyError> {
+ if !state
+ .snapshots
+ .contains_key(&(specifier.clone().into(), version.clone().into()))
+ {
+ let s = ModuleSpecifier::resolve_url(&specifier)?;
+ let file_cache = state.server_state.file_cache.read().unwrap();
+ let file_id = file_cache.lookup(&s).unwrap();
+ let content = file_cache.get_contents(file_id)?;
+ state
+ .snapshots
+ .insert((specifier.into(), version.into()), content);
+ }
+ Ok(())
+}
+
+fn op<F>(op_fn: F) -> Box<OpFn>
+where
+ F: Fn(&mut State, Value) -> Result<Value, AnyError> + 'static,
+{
+ json_op_sync(move |s, args, _bufs| {
+ let state = s.borrow_mut::<State>();
+ op_fn(state, args)
+ })
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct SourceSnapshotArgs {
+ specifier: String,
+ version: String,
+}
+
+/// The language service is dropping a reference to a source file snapshot, and
+/// we can drop our version of that document.
+fn dispose(state: &mut State, args: Value) -> Result<Value, AnyError> {
+ let v: SourceSnapshotArgs = serde_json::from_value(args)?;
+ state
+ .snapshots
+ .remove(&(v.specifier.into(), v.version.into()));
+ Ok(json!(true))
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct GetChangeRangeArgs {
+ specifier: String,
+ old_length: u32,
+ old_version: String,
+ version: String,
+}
+
+/// The language service wants to compare an old snapshot with a new snapshot to
+/// determine what source hash changed.
+fn get_change_range(state: &mut State, args: Value) -> Result<Value, AnyError> {
+ let v: GetChangeRangeArgs = serde_json::from_value(args.clone())?;
+ cache_snapshot(state, v.specifier.clone(), v.version.clone())?;
+ if let Some(current) = state
+ .snapshots
+ .get(&(v.specifier.clone().into(), v.version.into()))
+ {
+ if let Some(prev) = state
+ .snapshots
+ .get(&(v.specifier.clone().into(), v.old_version.clone().into()))
+ {
+ Ok(text::get_range_change(prev, current))
+ } else {
+ // when a local file is opened up in the editor, the compiler might
+ // already have a snapshot of it in memory, and will request it, but we
+ // now are working off in memory versions of the document, and so need
+ // to tell tsc to reset the whole document
+ Ok(json!({
+ "span": {
+ "start": 0,
+ "length": v.old_length,
+ },
+ "newLength": current.chars().count(),
+ }))
+ }
+ } else {
+ Err(custom_error(
+ "MissingSnapshot",
+ format!(
+ "The current snapshot version is missing.\n Args: \"{}\"",
+ args
+ ),
+ ))
+ }
+}
+
+fn get_length(state: &mut State, args: Value) -> Result<Value, AnyError> {
+ let v: SourceSnapshotArgs = serde_json::from_value(args)?;
+ let specifier = ModuleSpecifier::resolve_url(&v.specifier)?;
+ if state.server_state.doc_data.contains_key(&specifier) {
+ cache_snapshot(state, v.specifier.clone(), v.version.clone())?;
+ let content = state
+ .snapshots
+ .get(&(v.specifier.into(), v.version.into()))
+ .unwrap();
+ Ok(json!(content.chars().count()))
+ } else {
+ let mut sources = state.server_state.sources.write().unwrap();
+ Ok(json!(sources.get_length(&specifier).unwrap()))
+ }
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct GetTextArgs {
+ specifier: String,
+ version: String,
+ start: usize,
+ end: usize,
+}
+
+fn get_text(state: &mut State, args: Value) -> Result<Value, AnyError> {
+ let v: GetTextArgs = serde_json::from_value(args)?;
+ let specifier = ModuleSpecifier::resolve_url(&v.specifier)?;
+ let content = if state.server_state.doc_data.contains_key(&specifier) {
+ cache_snapshot(state, v.specifier.clone(), v.version.clone())?;
+ state
+ .snapshots
+ .get(&(v.specifier.into(), v.version.into()))
+ .unwrap()
+ .clone()
+ } else {
+ let mut sources = state.server_state.sources.write().unwrap();
+ sources.get_text(&specifier).unwrap()
+ };
+ Ok(json!(text::slice(&content, v.start..v.end)))
+}
+
+fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> {
+ let v: ResolveArgs = serde_json::from_value(args)?;
+ let mut resolved = Vec::<Option<(String, String)>>::new();
+ let referrer = ModuleSpecifier::resolve_url(&v.base)?;
+ let mut sources = if let Ok(sources) = state.server_state.sources.write() {
+ sources
+ } else {
+ return Err(custom_error("Deadlock", "deadlock locking sources"));
+ };
+
+ if let Some(doc_data) = state.server_state.doc_data.get(&referrer) {
+ if let Some(dependencies) = &doc_data.dependencies {
+ for specifier in &v.specifiers {
+ if specifier.starts_with("asset:///") {
+ resolved.push(Some((
+ specifier.clone(),
+ MediaType::from(specifier).as_ts_extension(),
+ )))
+ } else if let Some(dependency) = dependencies.get(specifier) {
+ let resolved_import =
+ if let Some(resolved_import) = &dependency.maybe_type {
+ resolved_import.clone()
+ } else if let Some(resolved_import) = &dependency.maybe_code {
+ resolved_import.clone()
+ } else {
+ ResolvedImport::Err("missing dependency".to_string())
+ };
+ if let ResolvedImport::Resolved(resolved_specifier) = resolved_import
+ {
+ let media_type = if let Some(media_type) =
+ sources.get_media_type(&resolved_specifier)
+ {
+ media_type
+ } else {
+ MediaType::from(&resolved_specifier)
+ };
+ resolved.push(Some((
+ resolved_specifier.to_string(),
+ media_type.as_ts_extension(),
+ )));
+ } else {
+ resolved.push(None);
+ }
+ }
+ }
+ }
+ } else if sources.contains(&referrer) {
+ for specifier in &v.specifiers {
+ if let Some((resolved_specifier, media_type)) =
+ sources.resolve_import(specifier, &referrer)
+ {
+ resolved.push(Some((
+ resolved_specifier.to_string(),
+ media_type.as_ts_extension(),
+ )));
+ } else {
+ resolved.push(None);
+ }
+ }
+ } else {
+ return Err(custom_error(
+ "NotFound",
+ "the referring specifier is unexpectedly missing",
+ ));
+ }
+
+ Ok(json!(resolved))
+}
+
+fn respond(state: &mut State, args: Value) -> Result<Value, AnyError> {
+ state.response = Some(serde_json::from_value(args)?);
+ Ok(json!(true))
+}
+
+fn script_names(state: &mut State, _args: Value) -> Result<Value, AnyError> {
+ let script_names: Vec<&ModuleSpecifier> =
+ state.server_state.doc_data.keys().collect();
+ Ok(json!(script_names))
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct ScriptVersionArgs {
+ specifier: String,
+}
+
+fn script_version(state: &mut State, args: Value) -> Result<Value, AnyError> {
+ let v: ScriptVersionArgs = serde_json::from_value(args)?;
+ let specifier = ModuleSpecifier::resolve_url(&v.specifier)?;
+ let maybe_doc_data = state.server_state.doc_data.get(&specifier);
+ if let Some(doc_data) = maybe_doc_data {
+ if let Some(version) = doc_data.version {
+ return Ok(json!(version.to_string()));
+ }
+ } else {
+ let mut sources = state.server_state.sources.write().unwrap();
+ if let Some(version) = sources.get_script_version(&specifier) {
+ return Ok(json!(version));
+ }
+ }
+
+ Ok(json!(None::<String>))
+}
+
+/// Create and setup a JsRuntime based on a snapshot. It is expected that the
+/// supplied snapshot is an isolate that contains the TypeScript language
+/// server.
+pub fn start(debug: bool) -> Result<JsRuntime, AnyError> {
+ let mut runtime = JsRuntime::new(RuntimeOptions {
+ startup_snapshot: Some(js::compiler_isolate_init()),
+ ..Default::default()
+ });
+
+ {
+ let op_state = runtime.op_state();
+ let mut op_state = op_state.borrow_mut();
+ op_state.put(State::new(ServerStateSnapshot::default()));
+ }
+
+ runtime.register_op("op_dispose", op(dispose));
+ runtime.register_op("op_get_change_range", op(get_change_range));
+ runtime.register_op("op_get_length", op(get_length));
+ runtime.register_op("op_get_text", op(get_text));
+ runtime.register_op("op_resolve", op(resolve));
+ runtime.register_op("op_respond", op(respond));
+ runtime.register_op("op_script_names", op(script_names));
+ runtime.register_op("op_script_version", op(script_version));
+
+ let init_config = json!({ "debug": debug });
+ let init_src = format!("globalThis.serverInit({});", init_config);
+
+ runtime.execute("[native code]", &init_src)?;
+ Ok(runtime)
+}
+
+/// Methods that are supported by the Language Service in the compiler isolate.
+pub enum RequestMethod {
+ /// Configure the compilation settings for the server.
+ Configure(TsConfig),
+ /// Return semantic diagnostics for given file.
+ GetSemanticDiagnostics(ModuleSpecifier),
+ /// Returns suggestion diagnostics for given file.
+ GetSuggestionDiagnostics(ModuleSpecifier),
+ /// Return syntactic diagnostics for a given file.
+ GetSyntacticDiagnostics(ModuleSpecifier),
+ /// Return quick info at position (hover information).
+ GetQuickInfo((ModuleSpecifier, u32)),
+ /// Return document highlights at position.
+ GetDocumentHighlights((ModuleSpecifier, u32, Vec<ModuleSpecifier>)),
+ /// Get document references for a specific position.
+ GetReferences((ModuleSpecifier, u32)),
+ /// Get declaration information for a specific position.
+ GetDefinition((ModuleSpecifier, u32)),
+}
+
+impl RequestMethod {
+ pub fn to_value(&self, id: usize) -> Value {
+ match self {
+ RequestMethod::Configure(config) => json!({
+ "id": id,
+ "method": "configure",
+ "compilerOptions": config,
+ }),
+ RequestMethod::GetSemanticDiagnostics(specifier) => json!({
+ "id": id,
+ "method": "getSemanticDiagnostics",
+ "specifier": specifier,
+ }),
+ RequestMethod::GetSuggestionDiagnostics(specifier) => json!({
+ "id": id,
+ "method": "getSuggestionDiagnostics",
+ "specifier": specifier,
+ }),
+ RequestMethod::GetSyntacticDiagnostics(specifier) => json!({
+ "id": id,
+ "method": "getSyntacticDiagnostics",
+ "specifier": specifier,
+ }),
+ RequestMethod::GetQuickInfo((specifier, position)) => json!({
+ "id": id,
+ "method": "getQuickInfo",
+ "specifier": specifier,
+ "position": position,
+ }),
+ RequestMethod::GetDocumentHighlights((
+ specifier,
+ position,
+ files_to_search,
+ )) => json!({
+ "id": id,
+ "method": "getDocumentHighlights",
+ "specifier": specifier,
+ "position": position,
+ "filesToSearch": files_to_search,
+ }),
+ RequestMethod::GetReferences((specifier, position)) => json!({
+ "id": id,
+ "method": "getReferences",
+ "specifier": specifier,
+ "position": position,
+ }),
+ RequestMethod::GetDefinition((specifier, position)) => json!({
+ "id": id,
+ "method": "getDefinition",
+ "specifier": specifier,
+ "position": position,
+ }),
+ }
+ }
+}
+
+/// Send a request into a runtime and return the JSON value of the response.
+pub fn request(
+ runtime: &mut JsRuntime,
+ server_state: &ServerStateSnapshot,
+ method: RequestMethod,
+) -> Result<Value, AnyError> {
+ let id = {
+ let op_state = runtime.op_state();
+ let mut op_state = op_state.borrow_mut();
+ let state = op_state.borrow_mut::<State>();
+ state.server_state = server_state.clone();
+ state.last_id += 1;
+ state.last_id
+ };
+ let request_params = method.to_value(id);
+ let request_src = format!("globalThis.serverRequest({});", request_params);
+ runtime.execute("[native_code]", &request_src)?;
+
+ let op_state = runtime.op_state();
+ let mut op_state = op_state.borrow_mut();
+ let state = op_state.borrow_mut::<State>();
+
+ if let Some(response) = state.response.clone() {
+ state.response = None;
+ Ok(response.data)
+ } else {
+ Err(custom_error(
+ "RequestError",
+ "The response was not received for the request.",
+ ))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::super::memory_cache::MemoryCache;
+ use super::super::state::DocumentData;
+ use super::*;
+ use std::collections::HashMap;
+ use std::sync::Arc;
+ use std::sync::RwLock;
+
+ fn mock_server_state(sources: Vec<(&str, &str, i32)>) -> ServerStateSnapshot {
+ let mut doc_data = HashMap::new();
+ let mut file_cache = MemoryCache::default();
+ for (specifier, content, version) in sources {
+ let specifier = ModuleSpecifier::resolve_url(specifier)
+ .expect("failed to create specifier");
+ doc_data.insert(
+ specifier.clone(),
+ DocumentData::new(specifier.clone(), version, content, None),
+ );
+ file_cache.set_contents(specifier, Some(content.as_bytes().to_vec()));
+ }
+ let file_cache = Arc::new(RwLock::new(file_cache));
+ ServerStateSnapshot {
+ config: Default::default(),
+ diagnostics: Default::default(),
+ doc_data,
+ file_cache,
+ sources: Default::default(),
+ }
+ }
+
+ fn setup(
+ debug: bool,
+ config: Value,
+ sources: Vec<(&str, &str, i32)>,
+ ) -> (JsRuntime, ServerStateSnapshot) {
+ let server_state = mock_server_state(sources.clone());
+ let mut runtime = start(debug).expect("could not start server");
+ let ts_config = TsConfig::new(config);
+ assert_eq!(
+ request(
+ &mut runtime,
+ &server_state,
+ RequestMethod::Configure(ts_config)
+ )
+ .expect("failed request"),
+ json!(true)
+ );
+ (runtime, server_state)
+ }
+
+ #[test]
+ fn test_replace_links() {
+ let actual = replace_links(r"test {@link http://deno.land/x/mod.ts} test");
+ assert_eq!(
+ actual,
+ r"test [http://deno.land/x/mod.ts](http://deno.land/x/mod.ts) test"
+ );
+ let actual =
+ replace_links(r"test {@link http://deno.land/x/mod.ts a link} test");
+ assert_eq!(actual, r"test [a link](http://deno.land/x/mod.ts) test");
+ let actual =
+ replace_links(r"test {@linkcode http://deno.land/x/mod.ts a link} test");
+ assert_eq!(actual, r"test [`a link`](http://deno.land/x/mod.ts) test");
+ }
+
+ #[test]
+ fn test_project_configure() {
+ setup(
+ false,
+ json!({
+ "target": "esnext",
+ "module": "esnext",
+ "noEmit": true,
+ }),
+ vec![],
+ );
+ }
+
+ #[test]
+ fn test_project_reconfigure() {
+ let (mut runtime, server_state) = setup(
+ false,
+ json!({
+ "target": "esnext",
+ "module": "esnext",
+ "noEmit": true,
+ }),
+ vec![],
+ );
+ let ts_config = TsConfig::new(json!({
+ "target": "esnext",
+ "module": "esnext",
+ "noEmit": true,
+ "lib": ["deno.ns", "deno.worker"]
+ }));
+ let result = request(
+ &mut runtime,
+ &server_state,
+ RequestMethod::Configure(ts_config),
+ );
+ assert!(result.is_ok());
+ let response = result.unwrap();
+ assert_eq!(response, json!(true));
+ }
+
+ #[test]
+ fn test_get_semantic_diagnostics() {
+ let (mut runtime, server_state) = setup(
+ false,
+ json!({
+ "target": "esnext",
+ "module": "esnext",
+ "noEmit": true,
+ }),
+ vec![("file:///a.ts", r#"console.log("hello deno");"#, 1)],
+ );
+ let specifier = ModuleSpecifier::resolve_url("file:///a.ts")
+ .expect("could not resolve url");
+ let result = request(
+ &mut runtime,
+ &server_state,
+ RequestMethod::GetSemanticDiagnostics(specifier),
+ );
+ assert!(result.is_ok());
+ let response = result.unwrap();
+ assert_eq!(
+ response,
+ json!([
+ {
+ "start": {
+ "line": 0,
+ "character": 0,
+ },
+ "end": {
+ "line": 0,
+ "character": 7
+ },
+ "fileName": "file:///a.ts",
+ "messageText": "Cannot find name 'console'. Do you need to change your target library? Try changing the `lib` compiler option to include 'dom'.",
+ "sourceLine": "console.log(\"hello deno\");",
+ "category": 1,
+ "code": 2584
+ }
+ ])
+ );
+ }
+
+ #[test]
+ fn test_module_resolution() {
+ let (mut runtime, server_state) = setup(
+ false,
+ json!({
+ "target": "esnext",
+ "module": "esnext",
+ "lib": ["deno.ns", "deno.window"],
+ "noEmit": true,
+ }),
+ vec![(
+ "file:///a.ts",
+ r#"
+ import { B } from "https://deno.land/x/b/mod.ts";
+
+ const b = new B();
+
+ console.log(b);
+ "#,
+ 1,
+ )],
+ );
+ let specifier = ModuleSpecifier::resolve_url("file:///a.ts")
+ .expect("could not resolve url");
+ let result = request(
+ &mut runtime,
+ &server_state,
+ RequestMethod::GetSemanticDiagnostics(specifier),
+ );
+ assert!(result.is_ok());
+ let response = result.unwrap();
+ assert_eq!(response, json!([]));
+ }
+
+ #[test]
+ fn test_bad_module_specifiers() {
+ let (mut runtime, server_state) = setup(
+ false,
+ json!({
+ "target": "esnext",
+ "module": "esnext",
+ "lib": ["deno.ns", "deno.window"],
+ "noEmit": true,
+ }),
+ vec![(
+ "file:///a.ts",
+ r#"
+ import { A } from ".";
+ "#,
+ 1,
+ )],
+ );
+ let specifier = ModuleSpecifier::resolve_url("file:///a.ts")
+ .expect("could not resolve url");
+ let result = request(
+ &mut runtime,
+ &server_state,
+ RequestMethod::GetSyntacticDiagnostics(specifier),
+ );
+ assert!(result.is_ok());
+ let response = result.unwrap();
+ assert_eq!(response, json!([]));
+ }
+
+ #[test]
+ fn test_remote_modules() {
+ let (mut runtime, server_state) = setup(
+ false,
+ json!({
+ "target": "esnext",
+ "module": "esnext",
+ "lib": ["deno.ns", "deno.window"],
+ "noEmit": true,
+ }),
+ vec![(
+ "file:///a.ts",
+ r#"
+ import { B } from "https://deno.land/x/b/mod.ts";
+
+ const b = new B();
+
+ console.log(b);
+ "#,
+ 1,
+ )],
+ );
+ let specifier = ModuleSpecifier::resolve_url("file:///a.ts")
+ .expect("could not resolve url");
+ let result = request(
+ &mut runtime,
+ &server_state,
+ RequestMethod::GetSyntacticDiagnostics(specifier),
+ );
+ assert!(result.is_ok());
+ let response = result.unwrap();
+ assert_eq!(response, json!([]));
+ }
+
+ #[test]
+ fn test_partial_modules() {
+ let (mut runtime, server_state) = setup(
+ false,
+ json!({
+ "target": "esnext",
+ "module": "esnext",
+ "lib": ["deno.ns", "deno.window"],
+ "noEmit": true,
+ }),
+ vec![(
+ "file:///a.ts",
+ r#"
+ import {
+ Application,
+ Context,
+ Router,
+ Status,
+ } from "https://deno.land/x/oak@v6.3.2/mod.ts";
+
+ import * as test from
+ "#,
+ 1,
+ )],
+ );
+ let specifier = ModuleSpecifier::resolve_url("file:///a.ts")
+ .expect("could not resolve url");
+ let result = request(
+ &mut runtime,
+ &server_state,
+ RequestMethod::GetSyntacticDiagnostics(specifier),
+ );
+ println!("{:?}", result);
+ // assert!(result.is_ok());
+ // let response = result.unwrap();
+ // assert_eq!(response, json!([]));
+ }
+}
diff --git a/cli/lsp/utils.rs b/cli/lsp/utils.rs
new file mode 100644
index 000000000..0c3d5a635
--- /dev/null
+++ b/cli/lsp/utils.rs
@@ -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()
+ );
+ }
+}
diff --git a/cli/main.rs b/cli/main.rs
index e297d0c4c..2e40df66b 100644
--- a/cli/main.rs
+++ b/cli/main.rs
@@ -28,6 +28,7 @@ mod info;
mod inspector;
mod js;
mod lockfile;
+mod lsp;
mod media_type;
mod metrics;
mod module_graph;
@@ -258,6 +259,10 @@ async fn install_command(
tools::installer::install(flags, &module_url, args, name, root, force)
}
+async fn language_server_command() -> Result<(), AnyError> {
+ lsp::start()
+}
+
async fn lint_command(
flags: Flags,
files: Vec<PathBuf>,
@@ -992,6 +997,7 @@ fn get_subcommand(
} => {
install_command(flags, module_url, args, name, root, force).boxed_local()
}
+ DenoSubcommand::LanguageServer => language_server_command().boxed_local(),
DenoSubcommand::Lint {
files,
rules,
diff --git a/cli/module_graph.rs b/cli/module_graph.rs
index 8c6f69552..4144ee5ee 100644
--- a/cli/module_graph.rs
+++ b/cli/module_graph.rs
@@ -31,6 +31,8 @@ use crate::AnyError;
use deno_core::error::Context;
use deno_core::futures::stream::FuturesUnordered;
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::Serializer;
use deno_core::serde_json::json;
@@ -38,8 +40,6 @@ use deno_core::serde_json::Value;
use deno_core::ModuleResolutionError;
use deno_core::ModuleSpecifier;
use regex::Regex;
-use serde::Deserialize;
-use serde::Deserializer;
use std::cell::RefCell;
use std::collections::HashSet;
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.
#[derive(Debug, Clone, Eq, PartialEq)]
-enum TypeScriptReference {
+pub enum TypeScriptReference {
Path(String),
Types(String),
}
/// Determine if a comment contains a triple slash reference and optionally
/// 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) {
None
} 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
/// 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(m) = captures.get(1) {
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.
#[derive(Debug, Clone)]
-struct Module {
- dependencies: DependencyMap,
+pub struct Module {
+ pub dependencies: DependencyMap,
is_dirty: bool,
is_parsed: bool,
maybe_emit: Option<Emit>,
diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs
index aca2df99c..42172a771 100644
--- a/cli/tests/integration_tests.rs
+++ b/cli/tests/integration_tests.rs
@@ -914,7 +914,7 @@ fn ts_reload() {
assert!(std::str::from_utf8(&output.stdout)
.unwrap()
.trim()
- .contains("\"host.writeFile(\\\"deno://002_hello.js\\\")\""));
+ .contains("host.writeFile(\"deno://002_hello.js\")"));
}
#[test]
diff --git a/cli/tests/lsp/did_open_notification.json b/cli/tests/lsp/did_open_notification.json
new file mode 100644
index 000000000..04f12a7b3
--- /dev/null
+++ b/cli/tests/lsp/did_open_notification.json
@@ -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"
+ }
+ }
+}
diff --git a/cli/tests/lsp/exit_notification.json b/cli/tests/lsp/exit_notification.json
new file mode 100644
index 000000000..799a0d1d5
--- /dev/null
+++ b/cli/tests/lsp/exit_notification.json
@@ -0,0 +1,5 @@
+{
+ "jsonrpc": "2.0",
+ "method": "exit",
+ "params": null
+}
diff --git a/cli/tests/lsp/hover_request.json b/cli/tests/lsp/hover_request.json
new file mode 100644
index 000000000..f12bd52df
--- /dev/null
+++ b/cli/tests/lsp/hover_request.json
@@ -0,0 +1,14 @@
+{
+ "jsonrpc": "2.0",
+ "id": 2,
+ "method": "textDocument/hover",
+ "params": {
+ "textDocument": {
+ "uri": "file:///a/file.ts"
+ },
+ "position": {
+ "line": 0,
+ "character": 19
+ }
+ }
+}
diff --git a/cli/tests/lsp/initialize_request.json b/cli/tests/lsp/initialize_request.json
new file mode 100644
index 000000000..960420bfd
--- /dev/null
+++ b/cli/tests/lsp/initialize_request.json
@@ -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
+ }
+ }
+ }
+ }
+}
diff --git a/cli/tests/lsp/initialized_notification.json b/cli/tests/lsp/initialized_notification.json
new file mode 100644
index 000000000..972f8abc8
--- /dev/null
+++ b/cli/tests/lsp/initialized_notification.json
@@ -0,0 +1,5 @@
+{
+ "jsonrpc": "2.0",
+ "method": "initialized",
+ "params": {}
+}
diff --git a/cli/tests/lsp/shutdown_request.json b/cli/tests/lsp/shutdown_request.json
new file mode 100644
index 000000000..fd4d78460
--- /dev/null
+++ b/cli/tests/lsp/shutdown_request.json
@@ -0,0 +1,6 @@
+{
+ "jsonrpc": "2.0",
+ "id": 3,
+ "method": "shutdown",
+ "params": null
+}
diff --git a/cli/tests/lsp_tests.rs b/cli/tests/lsp_tests.rs
new file mode 100644
index 000000000..7de655ac8
--- /dev/null
+++ b/cli/tests/lsp_tests.rs
@@ -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"));
+}
diff --git a/cli/tests/type_directives_01.ts.out b/cli/tests/type_directives_01.ts.out
index 8d285d3a8..77ed3ae26 100644
--- a/cli/tests/type_directives_01.ts.out
+++ b/cli/tests/type_directives_01.ts.out
@@ -1,3 +1,3 @@
[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] \ No newline at end of file
diff --git a/cli/tests/type_directives_02.ts.out b/cli/tests/type_directives_02.ts.out
index aea1d4fd0..7949dfab5 100644
--- a/cli/tests/type_directives_02.ts.out
+++ b/cli/tests/type_directives_02.ts.out
@@ -1,3 +1,3 @@
[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] \ No newline at end of file
diff --git a/cli/tools/lint.rs b/cli/tools/lint.rs
index c40dcfd54..dc9a51a89 100644
--- a/cli/tools/lint.rs
+++ b/cli/tools/lint.rs
@@ -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()
.ignore_file_directive("deno-lint-ignore-file")
.ignore_diagnostic_directive("deno-lint-ignore")
diff --git a/cli/tsc.rs b/cli/tsc.rs
index 36668f6f7..69373b2fa 100644
--- a/cli/tsc.rs
+++ b/cli/tsc.rs
@@ -284,12 +284,12 @@ fn load(state: &mut State, args: Value) -> Result<Value, AnyError> {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
-struct ResolveArgs {
+pub struct ResolveArgs {
/// The base specifier that the supplied specifier strings should be resolved
/// relative to.
- base: String,
+ pub base: String,
/// 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> {
diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js
index bb8458c93..f379d6bae 100644
--- a/cli/tsc/99_main_compiler.js
+++ b/cli/tsc/99_main_compiler.js
@@ -1,5 +1,7 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
+// @ts-check
+/// <reference path="./compiler.d.ts" />
// deno-lint-ignore-file no-undef
// This module is the entry point for "compiler" isolate, ie. the one
@@ -11,6 +13,7 @@
delete Object.prototype.__proto__;
((window) => {
+ /** @type {DenoCore} */
const core = window.Deno.core;
let logDebug = false;
@@ -25,7 +28,9 @@ delete Object.prototype.__proto__;
function debug(...args) {
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`);
}
}
@@ -86,6 +91,7 @@ delete Object.prototype.__proto__;
/** @param {ts.Diagnostic[]} diagnostics */
function fromTypeScriptDiagnostic(diagnostics) {
return diagnostics.map(({ relatedInformation: ri, source, ...diag }) => {
+ /** @type {any} */
const value = fromRelatedInformation(diag);
value.relatedInformation = ri
? ri.map(fromRelatedInformation)
@@ -106,7 +112,7 @@ delete Object.prototype.__proto__;
* Deno, as they provide misleading or incorrect information. */
const IGNORED_DIAGNOSTICS = [
// 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.
1208,
// 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,
};
+ 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
* specific "bindings" to the Deno environment that tsc needs to work.
*
- * @type {ts.CompilerHost} */
+ * @type {ts.CompilerHost & ts.LanguageServiceHost} */
const host = {
fileExists(fileName) {
debug(`host.fileExists("${fileName}")`);
@@ -231,21 +299,73 @@ delete Object.prototype.__proto__;
debug(`host.resolveModuleNames()`);
debug(` base: ${base}`);
debug(` specifiers: ${specifiers.join(", ")}`);
- /** @type {Array<[string, ts.Extension]>} */
+ /** @type {Array<[string, ts.Extension] | undefined>} */
const resolved = core.jsonOpSync("op_resolve", {
specifiers,
base,
});
- const r = resolved.map(([resolvedFileName, extension]) => ({
- resolvedFileName,
- extension,
- isExternalLibraryImport: false,
- }));
- return r;
+ if (resolved) {
+ const result = resolved.map((item) => {
+ if (item) {
+ const [resolvedFileName, extension] = item;
+ return {
+ resolvedFileName,
+ extension,
+ isExternalLibraryImport: false,
+ };
+ }
+ return undefined;
+ });
+ result.length = specifiers.length;
+ return result;
+ } else {
+ return new Array(specifiers.length);
+ }
},
createHash(data) {
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]>} */
@@ -254,10 +374,13 @@ delete Object.prototype.__proto__;
function performanceStart() {
stats.length = 0;
- statsStart = new Date();
+ statsStart = Date.now();
ts.performance.enable();
}
+ /**
+ * @param {{ program: ts.Program | ts.EmitAndSemanticDiagnosticsBuilderProgram, fileCount?: number }} options
+ */
function performanceProgram({ program, fileCount }) {
if (program) {
if ("getProgram" in program) {
@@ -286,7 +409,7 @@ delete Object.prototype.__proto__;
}
function performanceEnd() {
- const duration = new Date() - statsStart;
+ const duration = Date.now() - statsStart;
stats.push(["Compile time", duration]);
return stats;
}
@@ -308,7 +431,7 @@ delete Object.prototype.__proto__;
debug(config);
const { options, errors: configFileParsingDiagnostics } = ts
- .convertCompilerOptionsFromJson(config, "", "tsconfig.json");
+ .convertCompilerOptionsFromJson(config, "");
// 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
// URLs which Deno supports. So we need to either ignore the diagnostic, or
@@ -340,6 +463,106 @@ delete Object.prototype.__proto__;
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;
/** Startup the runtime environment, setting various flags.
@@ -391,4 +614,9 @@ delete Object.prototype.__proto__;
// checking TypeScript.
globalThis.startup = startup;
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);
diff --git a/cli/tsc/compiler.d.ts b/cli/tsc/compiler.d.ts
new file mode 100644
index 000000000..1a899c291
--- /dev/null
+++ b/cli/tsc/compiler.d.ts
@@ -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;
+ }
+}
diff --git a/cli/tsc_config.rs b/cli/tsc_config.rs
index 773d2afb0..16661c768 100644
--- a/cli/tsc_config.rs
+++ b/cli/tsc_config.rs
@@ -52,7 +52,7 @@ impl fmt::Display for IgnoredCompilerOptions {
/// 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
/// in Deno.
-const IGNORED_COMPILER_OPTIONS: &[&str] = &[
+pub const IGNORED_COMPILER_OPTIONS: &[&str] = &[
"allowSyntheticDefaultImports",
"allowUmdGlobalAccess",
"baseUrl",
@@ -83,7 +83,7 @@ const IGNORED_COMPILER_OPTIONS: &[&str] = &[
"useDefineForClassFields",
];
-const IGNORED_RUNTIME_COMPILER_OPTIONS: &[&str] = &[
+pub const IGNORED_RUNTIME_COMPILER_OPTIONS: &[&str] = &[
"assumeChangesOnlyAffectDirectDependencies",
"build",
"charset",