From 301d3e4b6849d24154ac2d65c00a9b30223d000e Mon Sep 17 00:00:00 2001 From: Kitson Kelly Date: Mon, 7 Dec 2020 21:46:39 +1100 Subject: [PATCH] feat: add mvp language server (#8515) Resolves #8400 --- Cargo.lock | 66 +- cli/Cargo.toml | 22 +- cli/ast.rs | 2 +- cli/file_fetcher.rs | 4 +- cli/flags.rs | 38 +- cli/http_cache.rs | 2 +- cli/lsp/README.md | 23 + cli/lsp/analysis.rs | 324 +++++ cli/lsp/capabilities.rs | 59 + cli/lsp/config.rs | 49 + cli/lsp/diagnostics.rs | 268 ++++ cli/lsp/dispatch.rs | 185 +++ cli/lsp/handlers.rs | 266 ++++ cli/lsp/lsp_extensions.rs | 26 + cli/lsp/memory_cache.rs | 126 ++ cli/lsp/mod.rs | 415 +++++++ cli/lsp/sources.rs | 372 ++++++ cli/lsp/state.rs | 292 +++++ cli/lsp/text.rs | 514 ++++++++ cli/lsp/tsc.rs | 1210 +++++++++++++++++++ cli/lsp/utils.rs | 114 ++ cli/main.rs | 6 + cli/module_graph.rs | 14 +- cli/tests/integration_tests.rs | 2 +- cli/tests/lsp/did_open_notification.json | 12 + cli/tests/lsp/exit_notification.json | 5 + cli/tests/lsp/hover_request.json | 14 + cli/tests/lsp/initialize_request.json | 23 + cli/tests/lsp/initialized_notification.json | 5 + cli/tests/lsp/shutdown_request.json | 6 + cli/tests/lsp_tests.rs | 88 ++ cli/tests/type_directives_01.ts.out | 2 +- cli/tests/type_directives_02.ts.out | 2 +- cli/tools/lint.rs | 2 +- cli/tsc.rs | 6 +- cli/tsc/99_main_compiler.js | 254 +++- cli/tsc/compiler.d.ts | 103 ++ cli/tsc_config.rs | 4 +- 38 files changed, 4878 insertions(+), 47 deletions(-) create mode 100644 cli/lsp/README.md create mode 100644 cli/lsp/analysis.rs create mode 100644 cli/lsp/capabilities.rs create mode 100644 cli/lsp/config.rs create mode 100644 cli/lsp/diagnostics.rs create mode 100644 cli/lsp/dispatch.rs create mode 100644 cli/lsp/handlers.rs create mode 100644 cli/lsp/lsp_extensions.rs create mode 100644 cli/lsp/memory_cache.rs create mode 100644 cli/lsp/mod.rs create mode 100644 cli/lsp/sources.rs create mode 100644 cli/lsp/state.rs create mode 100644 cli/lsp/text.rs create mode 100644 cli/lsp/tsc.rs create mode 100644 cli/lsp/utils.rs create mode 100644 cli/tests/lsp/did_open_notification.json create mode 100644 cli/tests/lsp/exit_notification.json create mode 100644 cli/tests/lsp/hover_request.json create mode 100644 cli/tests/lsp/initialize_request.json create mode 100644 cli/tests/lsp/initialized_notification.json create mode 100644 cli/tests/lsp/shutdown_request.json create mode 100644 cli/tests/lsp_tests.rs create mode 100644 cli/tsc/compiler.d.ts diff --git a/Cargo.lock b/Cargo.lock index a34c10ed92..224d0f322b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -366,10 +366,20 @@ 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" @@ -381,6 +391,17 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d" +dependencies = [ + "autocfg 1.0.1", + "cfg-if 1.0.0", + "lazy_static", +] + [[package]] name = "darling" version = "0.10.2" @@ -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", @@ -1314,6 +1339,32 @@ dependencies = [ "cfg-if 0.1.10", ] +[[package]] +name = "lsp-server" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b18dfe0e4a380b872aa79d8e0ee6c3d7a9682466e84b83ad807c88b3545f79" +dependencies = [ + "crossbeam-channel 0.5.0", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "lsp-types" +version = "0.84.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b95be71fe205e44de754185bcf86447b65813ce1ceb298f8d3793ade5fff08d" +dependencies = [ + "base64 0.12.3", + "bitflags", + "serde", + "serde_json", + "serde_repr", + "url", +] + [[package]] name = "matches" version = "0.1.8" @@ -1513,7 +1564,7 @@ checksum = "77d03607cf88b4b160ba0e9ed425fff3cee3b55ac813f0c685b3a3772da37d0e" dependencies = [ "anymap", "bitflags", - "crossbeam-channel", + "crossbeam-channel 0.4.4", "filetime", "fsevent", "fsevent-sys", @@ -2282,6 +2333,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc6b7951b17b051f3210b063f12cc17320e2fe30ae05b0fe2a3abb068551c76" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.48", +] + [[package]] name = "serde_urlencoded" version = "0.6.1" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index a8d6e9c2b6..95ffac7fed 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 10d7b53830..ef64683106 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 0d11852d1f..5b2f6f74cb 100644 --- a/cli/file_fetcher.rs +++ b/cli/file_fetcher.rs @@ -131,7 +131,7 @@ fn fetch_local(specifier: &ModuleSpecifier) -> Result { /// 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, maybe_charset: Option, ) -> Result { @@ -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, ) -> (MediaType, Option) { diff --git a/cli/flags.rs b/cli/flags.rs index 5ff21971d2..2210d75656 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -17,6 +17,9 @@ pub enum DenoSubcommand { source_file: String, out_file: Option, }, + Cache { + files: Vec, + }, Compile { source_file: String, output: Option, @@ -35,9 +38,6 @@ pub enum DenoSubcommand { code: String, as_typescript: bool, }, - Cache { - files: Vec, - }, Fmt { check: bool, files: Vec, @@ -54,6 +54,7 @@ pub enum DenoSubcommand { root: Option, force: bool, }, + LanguageServer, Lint { files: Vec, ignore: Vec, @@ -293,6 +294,8 @@ pub fn flags_from_vec_safe(args: Vec) -> clap::Result { 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") @@ -1952,6 +1972,18 @@ mod tests { ); } + #[test] + fn language_server() { + let r = flags_from_vec_safe(svec!["deno", "lsp"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::LanguageServer, + ..Flags::default() + } + ); + } + #[test] fn lint() { let r = flags_from_vec_safe(svec![ diff --git a/cli/http_cache.rs b/cli/http_cache.rs index 9cf2adc1a5..dd5f4dc3fd 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 0000000000..dcc9532733 --- /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 0000000000..370b41c45f --- /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, + }, +} + +/// 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, 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, +) -> Vec { + 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, + pub maybe_type: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResolvedImport { + Resolved(ModuleSpecifier), + Err(String), +} + +pub fn resolve_import( + specifier: &str, + referrer: &ModuleSpecifier, + maybe_import_map: Option>>, +) -> 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>>, +) -> Option<(HashMap, Option)> { + 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::::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 0000000000..cf8f150cac --- /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 0000000000..ebc1457089 --- /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, + pub import_map: Option, + 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 0000000000..a7f027c1bb --- /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>, + versions: HashMap, + changes: HashSet, +} + +impl DiagnosticCollection { + pub fn set( + &mut self, + file_id: FileId, + source: DiagnosticSource, + version: Option, + diagnostics: Vec, + ) { + 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 { + self.map.get(&(file_id, source)).into_iter().flatten() + } + + pub fn get_version(&self, file_id: &FileId) -> Option { + self.versions.get(file_id).cloned() + } + + pub fn take_changes(&mut self) -> Option> { + if self.changes.is_empty() { + return None; + } + Some(mem::take(&mut self.changes)) + } +} + +pub type DiagnosticVec = Vec<(FileId, Option, Vec)>; + +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; + +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>, +) -> Option> { + 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, 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 { + 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 0000000000..774bdcef9b --- /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, + pub server_state: &'a mut ServerState, +} + +impl<'a> NotificationDispatcher<'a> { + pub fn on( + &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::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( + id: RequestId, + result: Result, +) -> 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, + 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( + &mut self, + f: fn(ServerStateSnapshot, R::Params) -> Result, + ) -> &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::() { + 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::(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( + &mut self, + f: fn(&mut ServerState, R::Params) -> Result, + ) -> 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::() { + 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::(id, result) + }) + .map_err(|_err| { + custom_error( + "SyncTaskPanic", + format!("sync task {:?} panicked", R::METHOD), + ) + })?; + self.server_state.respond(response); + Ok(self) + } + + fn parse(&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 0000000000..6dd7321c79 --- /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, 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>, 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>, 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> = + 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, 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 = + 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, 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 = + 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>, 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> = + 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 { + 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 0000000000..eb0a62464d --- /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 0000000000..75c5bdb251 --- /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, + vec: Vec, +} + +impl SpecifierInterner { + pub fn get(&self, specifier: &ModuleSpecifier) -> Option { + 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>>, + interner: SpecifierInterner, + changes: Vec, +} + +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> { + &self.data[file_id.0 as usize] + } + + pub fn get_contents(&self, file_id: FileId) -> Result { + 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> { + &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 { + self + .interner + .get(specifier) + .filter(|&it| self.get(it).is_some()) + } + + pub fn set_contents( + &mut self, + specifier: ModuleSpecifier, + contents: Option>, + ) { + 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 { + 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 0000000000..c26c5d89e0 --- /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 = 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::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::(|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::(|state, params| { + if params.text_document.uri.scheme() == "deno" { + // we can ignore virtual text documents opening, as they don't need to + // be tracked in memory, as they are static assets that won't change + // already managed by the language service + return Ok(()); + } + let specifier = utils::normalize_url(params.text_document.uri); + if state + .doc_data + .insert( + specifier.clone(), + DocumentData::new( + specifier.clone(), + params.text_document.version, + ¶ms.text_document.text, + None, + ), + ) + .is_some() + { + error!("duplicate DidOpenTextDocument: {}", specifier); + } + state + .file_cache + .write() + .unwrap() + .set_contents(specifier, Some(params.text_document.text.into_bytes())); + + Ok(()) + })? + .on::(|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::(|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::(|_state, _params| { + // nothing to do yet... cleanup things? + + Ok(()) + })? + .on::(|state, _params| { + state.send_request::( + 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::(|s, ()| { + s.shutdown_requested = true; + Ok(()) + })? + .on_sync::( + handlers::handle_document_highlight, + )? + .on_sync::( + handlers::handle_goto_definition, + )? + .on_sync::(handlers::handle_hover)? + .on_sync::(handlers::handle_references)? + .on::(handlers::handle_formatting) + .on::( + handlers::handle_virtual_text_document, + ) + .finish(); + + Ok(()) + } + + /// Start consuming events from the provided receiver channel. + pub fn run(mut self, inbox: Receiver) -> 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 0000000000..4f80044a29 --- /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>, + maybe_types: Option, + media_type: MediaType, + source: String, + version: String, +} + +#[derive(Debug, Clone, Default)] +pub struct Sources { + http_cache: HttpCache, + metadata: HashMap, + redirects: HashMap, + remotes: HashMap, +} + +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 { + 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> { + 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 { + let specifier = self.resolve_specifier(specifier)?; + let metadata = self.get_metadata(&specifier)?; + Some(metadata.media_type) + } + + fn get_metadata(&mut self, specifier: &ModuleSpecifier) -> Option { + 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 { + 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> { + 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 { + 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 { + 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 { + 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 { + 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 0000000000..18a1e4023d --- /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", ¬ification.method) + .finish() + }; + + match self { + Event::Message(Message::Notification(notification)) => { + if notification_is::( + notification, + ) || notification_is::( + 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>, + pub version: Option, + specifier: ModuleSpecifier, +} + +impl DocumentData { + pub fn new( + specifier: ModuleSpecifier, + version: i32, + source: &str, + maybe_import_map: Option>>, + ) -> 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>>, + ) { + 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, + pub file_cache: Arc>, + pub sources: Arc>, +} + +pub struct ServerState { + pub config: Config, + pub diagnostics: DiagnosticCollection, + pub doc_data: HashMap, + pub file_cache: Arc>, + req_queue: ReqQueue, + sender: Sender, + pub sources: Arc>, + pub shutdown_requested: bool, + pub status: Status, + task_sender: Sender, + pub task_receiver: Receiver, + pub ts_runtime: JsRuntime, +} + +impl ServerState { + pub fn new(sender: Sender, 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) -> Option { + 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( + &mut self, + params: N::Params, + ) { + let notification = Notification::new(N::METHOD.to_string(), params); + self.send(notification.into()); + } + + pub fn send_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(&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 0000000000..5bca534c1b --- /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 { + 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 { + 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, +) { + 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 { + let chunks = diff(a, b); + let mut text_edits = Vec::::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) -> &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 0000000000..65f6ebbdb3 --- /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>, +) -> Option { + maybe_parts.map(|parts| { + parts + .into_iter() + .map(|p| p.text) + .collect::>() + .join("") + }) +} + +fn get_tag_body_text(tag: &JSDocTagInfo) -> Option { + tag.text.as_ref().map(|text| match tag.name.as_str() { + "example" => { + let caption_regex = + Regex::new(r"(.*?)\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, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct QuickInfo { + kind: ScriptElementKind, + kind_modifiers: String, + text_span: TextSpan, + display_parts: Option>, + documentation: Option>, + tags: Option>, +} + +impl QuickInfo { + pub fn to_hover(&self, line_index: &[u32]) -> lsp_types::Hover { + let mut contents = Vec::::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::>() + .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, + is_in_string: Option, + text_span: TextSpan, + context_span: Option, + kind: HighlightSpanKind, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DefinitionInfo { + kind: ScriptElementKind, + name: String, + container_kind: Option, + container_name: Option, + text_span: TextSpan, + pub file_name: String, + original_text_span: Option, + original_file_name: Option, + context_span: Option, + original_context_span: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DefinitionInfoAndBoundSpan { + pub definitions: Option>, + text_span: TextSpan, +} + +impl DefinitionInfoAndBoundSpan { + pub fn to_definition( + &self, + line_index: &[u32], + mut index_provider: F, + ) -> Option + where + F: FnMut(ModuleSpecifier) -> Vec, + { + 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, +} + +impl DocumentHighlights { + pub fn to_highlight( + &self, + line_index: &[u32], + ) -> Vec { + 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, + text_span: TextSpan, + pub file_name: String, + original_text_span: Option, + original_file_name: Option, + context_span: Option, + original_context_span: Option, +} + +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, + 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(op_fn: F) -> Box +where + F: Fn(&mut State, Value) -> Result + 'static, +{ + json_op_sync(move |s, args, _bufs| { + let state = s.borrow_mut::(); + 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 { + 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 { + 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 { + 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 { + 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 { + let v: ResolveArgs = serde_json::from_value(args)?; + let mut resolved = Vec::>::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 { + state.response = Some(serde_json::from_value(args)?); + Ok(json!(true)) +} + +fn script_names(state: &mut State, _args: Value) -> Result { + 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 { + 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::)) +} + +/// 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 { + 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)), + /// 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 { + let id = { + let op_state = runtime.op_state(); + let mut op_state = op_state.borrow_mut(); + let state = op_state.borrow_mut::(); + 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::(); + + 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 0000000000..0c3d5a635c --- /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( + what: &'static str, + json: Value, +) -> Result { + 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::().is_some() +} + +pub fn notification_is( + 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 { + 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 e297d0c4c7..2e40df66bd 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, @@ -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 8c6f695523..4144ee5ee9 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 { +pub fn parse_ts_reference(comment: &str) -> Option { 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 { /// Determine if a comment contains a `@deno-types` pragma and optionally return /// its value. -fn parse_deno_types(comment: &str) -> Option { +pub fn parse_deno_types(comment: &str) -> Option { 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, diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index aca2df99c0..42172a7718 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 0000000000..04f12a7b3a --- /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 0000000000..799a0d1d53 --- /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 0000000000..f12bd52df6 --- /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 0000000000..960420bfd3 --- /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 0000000000..972f8abc8a --- /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 0000000000..fd4d784607 --- /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 0000000000..7de655ac80 --- /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 8d285d3a87..77ed3ae264 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 aea1d4fd08..7949dfab54 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 c40dcfd548..dc9a51a892 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>) -> Linter { +pub fn create_linter(syntax: Syntax, rules: Vec>) -> 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 36668f6f7d..69373b2fa8 100644 --- a/cli/tsc.rs +++ b/cli/tsc.rs @@ -284,12 +284,12 @@ fn load(state: &mut State, args: Value) -> Result { #[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, + pub specifiers: Vec, } fn resolve(state: &mut State, args: Value) -> Result { diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index bb8458c938..f379d6baeb 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 +/// // 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 0000000000..1a899c291a --- /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; + + 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(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; + } + + 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 773d2afb09..16661c7680 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",