diff options
-rw-r--r-- | Cargo.lock | 163 | ||||
-rw-r--r-- | cli/Cargo.toml | 5 | ||||
-rw-r--r-- | cli/lsp/README.md | 17 | ||||
-rw-r--r-- | cli/lsp/analysis.rs | 31 | ||||
-rw-r--r-- | cli/lsp/capabilities.rs | 26 | ||||
-rw-r--r-- | cli/lsp/config.rs | 9 | ||||
-rw-r--r-- | cli/lsp/diagnostics.rs | 201 | ||||
-rw-r--r-- | cli/lsp/dispatch.rs | 185 | ||||
-rw-r--r-- | cli/lsp/handlers.rs | 304 | ||||
-rw-r--r-- | cli/lsp/language_server.rs | 981 | ||||
-rw-r--r-- | cli/lsp/lsp_extensions.rs | 26 | ||||
-rw-r--r-- | cli/lsp/memory_cache.rs | 5 | ||||
-rw-r--r-- | cli/lsp/mod.rs | 469 | ||||
-rw-r--r-- | cli/lsp/sources.rs | 10 | ||||
-rw-r--r-- | cli/lsp/state.rs | 395 | ||||
-rw-r--r-- | cli/lsp/text.rs | 3 | ||||
-rw-r--r-- | cli/lsp/tsc.rs | 227 | ||||
-rw-r--r-- | cli/lsp/utils.rs | 62 | ||||
-rw-r--r-- | cli/main.rs | 2 | ||||
-rw-r--r-- | cli/tests/lsp_tests.rs | 88 | ||||
-rw-r--r-- | cli/tsc/99_main_compiler.js | 5 |
21 files changed, 1397 insertions, 1817 deletions
diff --git a/Cargo.lock b/Cargo.lock index 71ab5aec2..c760e19a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,6 +111,17 @@ dependencies = [ ] [[package]] +name = "async-trait" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.48", +] + +[[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -122,6 +133,18 @@ dependencies = [ ] [[package]] +name = "auto_impl" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cbf586c80ada5e5ccdecae80d3ef0854f224e2dd74435f8d87e6831b8d0a38" +dependencies = [ + "proc-macro-error", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.48", +] + +[[package]] name = "autocfg" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -366,21 +389,11 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" dependencies = [ - "crossbeam-utils 0.7.2", + "crossbeam-utils", "maybe-uninit", ] [[package]] -name = "crossbeam-channel" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" -dependencies = [ - "cfg-if 1.0.0", - "crossbeam-utils 0.8.1", -] - -[[package]] name = "crossbeam-utils" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -392,17 +405,6 @@ dependencies = [ ] [[package]] -name = "crossbeam-utils" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d" -dependencies = [ - "autocfg 1.0.1", - "cfg-if 1.0.0", - "lazy_static", -] - -[[package]] name = "darling" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -457,7 +459,6 @@ dependencies = [ "byteorder", "chrono", "clap", - "crossbeam-channel 0.5.0", "deno_core", "deno_doc", "deno_fetch", @@ -477,8 +478,7 @@ dependencies = [ "lazy_static", "libc", "log", - "lsp-server", - "lsp-types", + "lspower", "nix", "notify", "os_pipe", @@ -500,6 +500,7 @@ dependencies = [ "tokio 0.2.22", "tokio-rustls", "tokio-tungstenite", + "tower-test", "uuid", "walkdir", "winapi 0.3.9", @@ -1101,6 +1102,15 @@ dependencies = [ ] [[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + +[[package]] name = "hermit-abi" version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1377,29 +1387,51 @@ dependencies = [ ] [[package]] -name = "lsp-server" -version = "0.5.0" +name = "lsp-types" +version = "0.85.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b18dfe0e4a380b872aa79d8e0ee6c3d7a9682466e84b83ad807c88b3545f79" +checksum = "857650f3e83fb62f89d15410414e0ed7d0735445020da398d37f65d20a5423b9" dependencies = [ - "crossbeam-channel 0.5.0", - "log", + "base64 0.12.3", + "bitflags", "serde", "serde_json", + "serde_repr", + "url", ] [[package]] -name = "lsp-types" -version = "0.84.0" +name = "lspower" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b95be71fe205e44de754185bcf86447b65813ce1ceb298f8d3793ade5fff08d" +checksum = "64106b17ca8f6f73cc21a3d1f39684ff65293a291aa96026aee85eaae02339a5" dependencies = [ - "base64 0.12.3", - "bitflags", + "async-trait", + "auto_impl", + "bytes 0.5.6", + "dashmap", + "futures", + "log", + "lsp-types", + "lspower-macros", + "nom", "serde", "serde_json", - "serde_repr", - "url", + "tokio 0.2.22", + "tokio-util", + "tower-service", +] + +[[package]] +name = "lspower-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b77a3b4fcd1a014a7a7a1043a5c3646068abfc75b46a9f2c4ab813d53f7c3c" +dependencies = [ + "heck", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.48", ] [[package]] @@ -1601,7 +1633,7 @@ checksum = "77d03607cf88b4b160ba0e9ed425fff3cee3b55ac813f0c685b3a3772da37d0e" dependencies = [ "anymap", "bitflags", - "crossbeam-channel 0.4.4", + "crossbeam-channel", "filetime", "fsevent", "fsevent-sys", @@ -1877,6 +1909,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.48", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.7", + "version_check", +] + +[[package]] name = "proc-macro-hack" version = "0.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3037,6 +3093,17 @@ dependencies = [ ] [[package]] +name = "tokio-test" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0049c119b6d505c4447f5c64873636c7af6c75ab0d45fd9f618d82acb8016d" +dependencies = [ + "bytes 0.5.6", + "futures-core", + "tokio 0.2.22", +] + +[[package]] name = "tokio-tungstenite" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3073,12 +3140,32 @@ dependencies = [ ] [[package]] +name = "tower-layer" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a35d656f2638b288b33495d1053ea74c40dc05ec0b92084dd71ca5566c4ed1dc" + +[[package]] name = "tower-service" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" [[package]] +name = "tower-test" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba4bbc2c1e4a8543c30d4c13a4c8314ed72d6e07581910f665aa13fde0153c8" +dependencies = [ + "futures-util", + "pin-project 0.4.23", + "tokio 0.2.22", + "tokio-test", + "tower-layer", + "tower-service", +] + +[[package]] name = "tracing" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 80eeceef8..65f455215 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -40,7 +40,6 @@ atty = "0.2.14" base64 = "0.12.3" byteorder = "1.3.4" clap = "2.33.3" -crossbeam-channel = "0.5.0" dissimilar = "1.0.2" dprint-plugin-typescript = "0.35.1" encoding_rs = "0.8.24" @@ -52,8 +51,7 @@ jsonc-parser = "0.14.0" lazy_static = "1.4.0" libc = "0.2.77" log = "0.4.11" -lsp-server = "0.5.0" -lsp-types = { version = "0.84.0", features = ["proposed"] } +lspower = "0.1.0" notify = "5.0.0-pre.3" percent-encoding = "2.1.0" regex = "1.3.9" @@ -87,6 +85,7 @@ chrono = "0.4.15" os_pipe = "0.9.2" test_util = { path = "../test_util" } tokio-tungstenite = "0.11.0" +tower-test = "0.3.0" [target.'cfg(unix)'.dev-dependencies] exec = "0.3.1" # Used in test_raw_tty diff --git a/cli/lsp/README.md b/cli/lsp/README.md index dcc953273..87a662fc3 100644 --- a/cli/lsp/README.md +++ b/cli/lsp/README.md @@ -6,18 +6,11 @@ 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/). +> complete. This document gives an overview of the structure of the language +> server. ## 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. +When the language server is started, a `LanguageServer` instance is created +which holds all of the state of the language server. It also defines all of the +methods that the client calls via the Language Server RPC protocol. diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs index 95e21ed9a..7cf6aca37 100644 --- a/cli/lsp/analysis.rs +++ b/cli/lsp/analysis.rs @@ -11,12 +11,11 @@ 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 lspower::lsp_types; +use lspower::lsp_types::Position; +use lspower::lsp_types::Range; use std::collections::HashMap; use std::rc::Rc; -use std::sync::Arc; -use std::sync::RwLock; /// Category of self-generated diagnostic messages (those not coming from) /// TypeScript. @@ -114,13 +113,11 @@ pub enum ResolvedImport { pub fn resolve_import( specifier: &str, referrer: &ModuleSpecifier, - maybe_import_map: Option<Arc<RwLock<ImportMap>>>, + maybe_import_map: &Option<ImportMap>, ) -> ResolvedImport { let maybe_mapped = if let Some(import_map) = maybe_import_map { - if let Ok(maybe_specifier) = import_map - .read() - .unwrap() - .resolve(specifier, referrer.as_str()) + if let Ok(maybe_specifier) = + import_map.resolve(specifier, referrer.as_str()) { maybe_specifier } else { @@ -162,7 +159,7 @@ pub fn analyze_dependencies( specifier: &ModuleSpecifier, source: &str, media_type: &MediaType, - maybe_import_map: Option<Arc<RwLock<ImportMap>>>, + maybe_import_map: &Option<ImportMap>, ) -> Option<(HashMap<String, Dependency>, Option<ResolvedImport>)> { let specifier_str = specifier.to_string(); let source_map = Rc::new(swc_common::SourceMap::default()); @@ -179,12 +176,12 @@ pub fn analyze_dependencies( TypeScriptReference::Path(import) => { let dep = dependencies.entry(import.clone()).or_default(); let resolved_import = - resolve_import(&import, specifier, maybe_import_map.clone()); + resolve_import(&import, specifier, maybe_import_map); dep.maybe_code = Some(resolved_import); } TypeScriptReference::Types(import) => { let resolved_import = - resolve_import(&import, specifier, maybe_import_map.clone()); + resolve_import(&import, specifier, maybe_import_map); if media_type == &MediaType::JavaScript || media_type == &MediaType::JSX { @@ -204,17 +201,13 @@ pub fn analyze_dependencies( desc.kind != swc_ecmascript::dep_graph::DependencyKind::Require }) { let resolved_import = - resolve_import(&desc.specifier, specifier, maybe_import_map.clone()); + resolve_import(&desc.specifier, specifier, maybe_import_map); // 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(), - )) + Some(resolve_import(deno_types, specifier, maybe_import_map)) } else { None } @@ -291,7 +284,7 @@ mod tests { import * as React from "https://cdn.skypack.dev/react"; "#; let actual = - analyze_dependencies(&specifier, source, &MediaType::TypeScript, None); + analyze_dependencies(&specifier, source, &MediaType::TypeScript, &None); assert!(actual.is_some()); let (actual, maybe_type) = actual.unwrap(); assert!(maybe_type.is_none()); diff --git a/cli/lsp/capabilities.rs b/cli/lsp/capabilities.rs index 954baaf51..e43e6a7e2 100644 --- a/cli/lsp/capabilities.rs +++ b/cli/lsp/capabilities.rs @@ -5,16 +5,16 @@ ///! language server, which helps determine what messages are sent from the ///! client. ///! -use lsp_types::ClientCapabilities; -use lsp_types::CompletionOptions; -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; -use lsp_types::WorkDoneProgressOptions; +use lspower::lsp_types::ClientCapabilities; +use lspower::lsp_types::CompletionOptions; +use lspower::lsp_types::HoverProviderCapability; +use lspower::lsp_types::OneOf; +use lspower::lsp_types::SaveOptions; +use lspower::lsp_types::ServerCapabilities; +use lspower::lsp_types::TextDocumentSyncCapability; +use lspower::lsp_types::TextDocumentSyncKind; +use lspower::lsp_types::TextDocumentSyncOptions; +use lspower::lsp_types::WorkDoneProgressOptions; pub fn server_capabilities( _client_capabilities: &ClientCapabilities, @@ -61,16 +61,16 @@ pub fn server_capabilities( 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, + semantic_highlighting: None, + semantic_tokens_provider: None, + workspace: None, experimental: None, } } diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index fc3f030c9..b689275ef 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -1,10 +1,12 @@ // 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; use deno_core::url::Url; +use lspower::jsonrpc::Error as LSPError; +use lspower::jsonrpc::Result as LSPResult; +use lspower::lsp_types; #[derive(Debug, Clone, Default)] pub struct ClientCapabilities { @@ -29,8 +31,9 @@ pub struct Config { } impl Config { - pub fn update(&mut self, value: Value) -> Result<(), AnyError> { - let settings: WorkspaceSettings = serde_json::from_value(value)?; + pub fn update(&mut self, value: Value) -> LSPResult<()> { + let settings: WorkspaceSettings = serde_json::from_value(value) + .map_err(|err| LSPError::invalid_params(err.to_string()))?; self.settings = settings; Ok(()) } diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs index a7f027c1b..1d0a1fac9 100644 --- a/cli/lsp/diagnostics.rs +++ b/cli/lsp/diagnostics.rs @@ -2,8 +2,8 @@ use super::analysis::get_lint_references; use super::analysis::references_to_diagnostics; +use super::language_server::StateSnapshot; use super::memory_cache::FileId; -use super::state::ServerStateSnapshot; use super::tsc; use crate::diagnostics; @@ -12,52 +12,11 @@ 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 lspower::lsp_types; 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, @@ -108,41 +67,84 @@ impl DiagnosticCollection { pub type DiagnosticVec = Vec<(FileId, Option<i32>, Vec<lsp_types::Diagnostic>)>; -pub fn generate_linting_diagnostics( - state: &ServerStateSnapshot, +pub async fn generate_lint_diagnostics( + state_snapshot: StateSnapshot, + diagnostic_collection: DiagnosticCollection, ) -> 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())); + tokio::task::spawn_blocking(move || { + let mut diagnostic_list = Vec::new(); + + let file_cache = state_snapshot.file_cache.read().unwrap(); + for (specifier, doc_data) in state_snapshot.doc_data.iter() { + let file_id = file_cache.lookup(specifier).unwrap(); + let version = doc_data.version; + let current_version = diagnostic_collection.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() { + diagnostic_list.push(( + file_id, + version, + references_to_diagnostics(references), + )); + } else { + diagnostic_list.push((file_id, version, Vec::new())); + } } + } else { + error!("Missing file contents for: {}", specifier); } - } else { - error!("Missing file contents for: {}", specifier); } } + + diagnostic_list + }) + .await + .unwrap() +} + +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, + } } +} - diagnostics +fn to_lsp_range( + start: &diagnostics::Position, + end: &diagnostics::Position, +) -> lsp_types::Range { + lsp_types::Range { + start: start.into(), + end: end.into(), + } } type TsDiagnostics = Vec<diagnostics::Diagnostic>; @@ -168,7 +170,7 @@ fn to_lsp_related_information( if let (Some(source), Some(start), Some(end)) = (&ri.source, &ri.start, &ri.end) { - let uri = Url::parse(&source).unwrap(); + let uri = lsp_types::Url::parse(&source).unwrap(); Some(lsp_types::DiagnosticRelatedInformation { location: lsp_types::Location { uri, @@ -223,43 +225,36 @@ fn ts_json_to_diagnostics( ) } -pub fn generate_ts_diagnostics( - state: &ServerStateSnapshot, - runtime: &mut JsRuntime, +pub async fn generate_ts_diagnostics( + ts_server: &tsc::TsServer, + diagnostic_collection: &DiagnosticCollection, + state_snapshot: StateSnapshot, ) -> Result<DiagnosticVec, AnyError> { - if !state.config.settings.enable { - return Ok(Vec::new()); - } let mut diagnostics = Vec::new(); - let file_cache = state.file_cache.read().unwrap(); - for (specifier, doc_data) in state.doc_data.iter() { - let file_id = file_cache.lookup(specifier).unwrap(); + let state_snapshot_ = state_snapshot.clone(); + for (specifier, doc_data) in state_snapshot_.doc_data.iter() { + let file_id = { + // TODO(lucacasonato): this is highly inefficient + let file_cache = state_snapshot_.file_cache.read().unwrap(); + file_cache.lookup(specifier).unwrap() + }; let version = doc_data.version; - let current_version = state.diagnostics.get_version(&file_id); + let current_version = diagnostic_collection.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, - )?)?); + let req = tsc::RequestMethod::GetSemanticDiagnostics(specifier.clone()); + let mut ts_diagnostics = ts_json_to_diagnostics( + ts_server.request(state_snapshot.clone(), req).await?, + )?; + let req = tsc::RequestMethod::GetSuggestionDiagnostics(specifier.clone()); + ts_diagnostics.append(&mut ts_json_to_diagnostics( + ts_server.request(state_snapshot.clone(), req).await?, + )?); + let req = tsc::RequestMethod::GetSyntacticDiagnostics(specifier.clone()); + ts_diagnostics.append(&mut ts_json_to_diagnostics( + ts_server.request(state_snapshot.clone(), req).await?, + )?); diagnostics.push((file_id, version, ts_diagnostics)); } } diff --git a/cli/lsp/dispatch.rs b/cli/lsp/dispatch.rs deleted file mode 100644 index 774bdcef9..000000000 --- a/cli/lsp/dispatch.rs +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use super::state::ServerState; -use super::state::ServerStateSnapshot; -use super::state::Task; -use super::utils::from_json; -use super::utils::is_canceled; - -use deno_core::error::custom_error; -use deno_core::error::AnyError; -use lsp_server::ErrorCode; -use lsp_server::Notification; -use lsp_server::Request; -use lsp_server::RequestId; -use lsp_server::Response; -use serde::de::DeserializeOwned; -use serde::Serialize; -use std::fmt; -use std::panic; - -pub struct NotificationDispatcher<'a> { - pub notification: Option<Notification>, - pub server_state: &'a mut ServerState, -} - -impl<'a> NotificationDispatcher<'a> { - pub fn on<N>( - &mut self, - f: fn(&mut ServerState, N::Params) -> Result<(), AnyError>, - ) -> Result<&mut Self, AnyError> - where - N: lsp_types::notification::Notification + 'static, - N::Params: DeserializeOwned + Send + 'static, - { - let notification = match self.notification.take() { - Some(it) => it, - None => return Ok(self), - }; - let params = match notification.extract::<N::Params>(N::METHOD) { - Ok(it) => it, - Err(notification) => { - self.notification = Some(notification); - return Ok(self); - } - }; - f(self.server_state, params)?; - Ok(self) - } - - pub fn finish(&mut self) { - if let Some(notification) = &self.notification { - if !notification.method.starts_with("$/") { - error!("unhandled notification: {:?}", notification); - } - } - } -} - -fn result_to_response<R>( - id: RequestId, - result: Result<R::Result, AnyError>, -) -> Response -where - R: lsp_types::request::Request + 'static, - R::Params: DeserializeOwned + 'static, - R::Result: Serialize + 'static, -{ - match result { - Ok(response) => Response::new_ok(id, &response), - Err(err) => { - if is_canceled(&*err) { - Response::new_err( - id, - ErrorCode::ContentModified as i32, - "content modified".to_string(), - ) - } else { - Response::new_err(id, ErrorCode::InternalError as i32, err.to_string()) - } - } - } -} - -pub struct RequestDispatcher<'a> { - pub request: Option<Request>, - pub server_state: &'a mut ServerState, -} - -impl<'a> RequestDispatcher<'a> { - pub fn finish(&mut self) { - if let Some(request) = self.request.take() { - error!("unknown request: {:?}", request); - let response = Response::new_err( - request.id, - ErrorCode::MethodNotFound as i32, - "unknown request".to_string(), - ); - self.server_state.respond(response); - } - } - - /// Handle a request which will respond to the LSP client asynchronously via - /// a spawned thread. - pub fn on<R>( - &mut self, - f: fn(ServerStateSnapshot, R::Params) -> Result<R::Result, AnyError>, - ) -> &mut Self - where - R: lsp_types::request::Request + 'static, - R::Params: DeserializeOwned + Send + fmt::Debug + 'static, - R::Result: Serialize + 'static, - { - let (id, params) = match self.parse::<R>() { - Some(it) => it, - None => return self, - }; - self.server_state.spawn({ - let state = self.server_state.snapshot(); - move || { - let result = f(state, params); - Task::Response(result_to_response::<R>(id, result)) - } - }); - - self - } - - /// Handle a request which will respond synchronously, returning a result if - /// the request cannot be handled or has issues. - pub fn on_sync<R>( - &mut self, - f: fn(&mut ServerState, R::Params) -> Result<R::Result, AnyError>, - ) -> Result<&mut Self, AnyError> - where - R: lsp_types::request::Request + 'static, - R::Params: DeserializeOwned + panic::UnwindSafe + fmt::Debug + 'static, - R::Result: Serialize + 'static, - { - let (id, params) = match self.parse::<R>() { - Some(it) => it, - None => return Ok(self), - }; - let state = panic::AssertUnwindSafe(&mut *self.server_state); - - let response = panic::catch_unwind(move || { - let result = f(state.0, params); - result_to_response::<R>(id, result) - }) - .map_err(|_err| { - custom_error( - "SyncTaskPanic", - format!("sync task {:?} panicked", R::METHOD), - ) - })?; - self.server_state.respond(response); - Ok(self) - } - - fn parse<R>(&mut self) -> Option<(RequestId, R::Params)> - where - R: lsp_types::request::Request + 'static, - R::Params: DeserializeOwned + 'static, - { - let request = match &self.request { - Some(request) if request.method == R::METHOD => { - self.request.take().unwrap() - } - _ => return None, - }; - - let response = from_json(R::METHOD, request.params); - match response { - Ok(params) => Some((request.id, params)), - Err(err) => { - let response = Response::new_err( - request.id, - ErrorCode::InvalidParams as i32, - err.to_string(), - ); - self.server_state.respond(response); - None - } - } - } -} diff --git a/cli/lsp/handlers.rs b/cli/lsp/handlers.rs deleted file mode 100644 index 69cdd8041..000000000 --- a/cli/lsp/handlers.rs +++ /dev/null @@ -1,304 +0,0 @@ -// 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::CompletionParams; -use lsp_types::CompletionResponse; -use lsp_types::DocumentFormattingParams; -use lsp_types::DocumentHighlight; -use lsp_types::DocumentHighlightParams; -use lsp_types::GotoDefinitionParams; -use lsp_types::GotoDefinitionResponse; -use lsp_types::Hover; -use lsp_types::HoverParams; -use lsp_types::Location; -use lsp_types::ReferenceParams; -use lsp_types::TextEdit; -use std::path::PathBuf; - -fn get_line_index( - state: &mut ServerState, - specifier: &ModuleSpecifier, -) -> Result<Vec<u32>, AnyError> { - let line_index = if specifier.as_url().scheme() == "asset" { - let server_state = state.snapshot(); - if let Some(source) = - tsc::get_asset(specifier, &mut state.ts_runtime, &server_state)? - { - text::index_lines(&source) - } else { - return Err(custom_error( - "NotFound", - format!("asset source missing: {}", specifier), - )); - } - } else { - let file_cache = state.file_cache.read().unwrap(); - if let Some(file_id) = file_cache.lookup(specifier) { - let file_text = file_cache.get_contents(file_id)?; - text::index_lines(&file_text) - } else { - let mut sources = state.sources.write().unwrap(); - if let Some(line_index) = sources.get_line_index(specifier) { - line_index - } else { - return Err(custom_error( - "NotFound", - format!("source for specifier not found: {}", specifier), - )); - } - } - }; - Ok(line_index) -} - -pub fn handle_formatting( - state: ServerStateSnapshot, - params: DocumentFormattingParams, -) -> Result<Option<Vec<TextEdit>>, AnyError> { - let specifier = utils::normalize_url(params.text_document.uri.clone()); - let file_cache = state.file_cache.read().unwrap(); - let file_id = file_cache.lookup(&specifier).unwrap(); - let file_text = file_cache.get_contents(file_id)?; - - let file_path = if let Ok(file_path) = params.text_document.uri.to_file_path() - { - file_path - } else { - PathBuf::from(params.text_document.uri.path()) - }; - let config = dprint::configuration::ConfigurationBuilder::new() - .deno() - .build(); - - // TODO(@kitsonk) this could be handled better in `cli/tools/fmt.rs` in the - // future. - let new_text = dprint::format_text(&file_path, &file_text, &config) - .map_err(|e| custom_error("FormatError", e))?; - - let text_edits = text::get_edits(&file_text, &new_text); - if text_edits.is_empty() { - Ok(None) - } else { - Ok(Some(text_edits)) - } -} - -pub fn handle_document_highlight( - state: &mut ServerState, - params: DocumentHighlightParams, -) -> Result<Option<Vec<DocumentHighlight>>, AnyError> { - let specifier = utils::normalize_url( - params.text_document_position_params.text_document.uri, - ); - let line_index = get_line_index(state, &specifier)?; - let server_state = state.snapshot(); - let files_to_search = vec![specifier.clone()]; - let maybe_document_highlights: Option<Vec<tsc::DocumentHighlights>> = - serde_json::from_value(tsc::request( - &mut state.ts_runtime, - &server_state, - tsc::RequestMethod::GetDocumentHighlights(( - specifier, - text::to_char_pos( - &line_index, - params.text_document_position_params.position, - ), - files_to_search, - )), - )?)?; - - if let Some(document_highlights) = maybe_document_highlights { - Ok(Some( - document_highlights - .into_iter() - .map(|dh| dh.to_highlight(&line_index)) - .flatten() - .collect(), - )) - } else { - Ok(None) - } -} - -pub fn handle_goto_definition( - state: &mut ServerState, - params: GotoDefinitionParams, -) -> Result<Option<GotoDefinitionResponse>, AnyError> { - let specifier = utils::normalize_url( - params.text_document_position_params.text_document.uri, - ); - let line_index = get_line_index(state, &specifier)?; - let server_state = state.snapshot(); - let maybe_definition: Option<tsc::DefinitionInfoAndBoundSpan> = - serde_json::from_value(tsc::request( - &mut state.ts_runtime, - &server_state, - tsc::RequestMethod::GetDefinition(( - specifier, - text::to_char_pos( - &line_index, - params.text_document_position_params.position, - ), - )), - )?)?; - - if let Some(definition) = maybe_definition { - Ok( - definition - .to_definition(&line_index, |s| get_line_index(state, &s).unwrap()), - ) - } else { - Ok(None) - } -} - -pub fn handle_hover( - state: &mut ServerState, - params: HoverParams, -) -> Result<Option<Hover>, AnyError> { - let specifier = utils::normalize_url( - params.text_document_position_params.text_document.uri, - ); - let line_index = get_line_index(state, &specifier)?; - let server_state = state.snapshot(); - let maybe_quick_info: Option<tsc::QuickInfo> = - serde_json::from_value(tsc::request( - &mut state.ts_runtime, - &server_state, - tsc::RequestMethod::GetQuickInfo(( - specifier, - text::to_char_pos( - &line_index, - params.text_document_position_params.position, - ), - )), - )?)?; - - if let Some(quick_info) = maybe_quick_info { - Ok(Some(quick_info.to_hover(&line_index))) - } else { - Ok(None) - } -} - -pub fn handle_completion( - state: &mut ServerState, - params: CompletionParams, -) -> Result<Option<CompletionResponse>, 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_completion_info: Option<tsc::CompletionInfo> = - serde_json::from_value(tsc::request( - &mut state.ts_runtime, - &server_state, - tsc::RequestMethod::GetCompletions(( - specifier, - text::to_char_pos(&line_index, params.text_document_position.position), - tsc::UserPreferences { - // TODO(lucacasonato): enable this. see https://github.com/denoland/deno/pull/8651 - include_completions_with_insert_text: Some(false), - ..Default::default() - }, - )), - )?)?; - - if let Some(completions) = maybe_completion_info { - Ok(Some(completions.into_completion_response(&line_index))) - } else { - Ok(None) - } -} - -pub fn handle_references( - state: &mut ServerState, - params: ReferenceParams, -) -> Result<Option<Vec<Location>>, AnyError> { - let specifier = - utils::normalize_url(params.text_document_position.text_document.uri); - let line_index = get_line_index(state, &specifier)?; - let server_state = state.snapshot(); - let maybe_references: Option<Vec<tsc::ReferenceEntry>> = - serde_json::from_value(tsc::request( - &mut state.ts_runtime, - &server_state, - tsc::RequestMethod::GetReferences(( - specifier, - text::to_char_pos(&line_index, params.text_document_position.position), - )), - )?)?; - - if let Some(references) = maybe_references { - let mut results = Vec::new(); - for reference in references { - if !params.context.include_declaration && reference.is_definition { - continue; - } - let reference_specifier = - ModuleSpecifier::resolve_url(&reference.file_name).unwrap(); - let line_index = get_line_index(state, &reference_specifier)?; - results.push(reference.to_location(&line_index)); - } - - Ok(Some(results)) - } else { - Ok(None) - } -} - -pub fn handle_virtual_text_document( - state: &mut ServerState, - params: lsp_extensions::VirtualTextDocumentParams, -) -> Result<String, AnyError> { - let specifier = utils::normalize_url(params.text_document.uri); - let url = specifier.as_url(); - let contents = if url.as_str() == "deno:///status.md" { - let file_cache = state.file_cache.read().unwrap(); - format!( - r#"# Deno Language Server Status - -- Documents in memory: {} - -"#, - file_cache.len() - ) - } else { - match url.scheme() { - "asset" => { - let server_state = state.snapshot(); - if let Some(text) = - tsc::get_asset(&specifier, &mut state.ts_runtime, &server_state)? - { - text - } 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/language_server.rs b/cli/lsp/language_server.rs new file mode 100644 index 000000000..c1e3ac8d5 --- /dev/null +++ b/cli/lsp/language_server.rs @@ -0,0 +1,981 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::anyhow; +use deno_core::error::AnyError; +use deno_core::serde::Deserialize; +use deno_core::serde::Serialize; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::ModuleSpecifier; +use dprint_plugin_typescript as dprint; +use lspower::jsonrpc::Error as LSPError; +use lspower::jsonrpc::ErrorCode as LSPErrorCode; +use lspower::jsonrpc::Result as LSPResult; +use lspower::lsp_types::*; +use lspower::Client; +use std::collections::HashMap; +use std::env; +use std::path::PathBuf; +use std::sync::Arc; +use std::sync::RwLock; +use tokio::fs; + +use crate::deno_dir; +use crate::import_map::ImportMap; +use crate::media_type::MediaType; +use crate::tsc_config::TsConfig; + +use super::analysis; +use super::capabilities; +use super::config::Config; +use super::diagnostics; +use super::diagnostics::DiagnosticCollection; +use super::diagnostics::DiagnosticSource; +use super::memory_cache::MemoryCache; +use super::sources::Sources; +use super::text; +use super::text::apply_content_changes; +use super::tsc; +use super::tsc::TsServer; +use super::utils; + +#[derive(Debug, Clone)] +pub struct LanguageServer { + assets: Arc<RwLock<HashMap<ModuleSpecifier, Option<String>>>>, + client: Client, + ts_server: TsServer, + config: Arc<RwLock<Config>>, + doc_data: Arc<RwLock<HashMap<ModuleSpecifier, DocumentData>>>, + file_cache: Arc<RwLock<MemoryCache>>, + sources: Arc<RwLock<Sources>>, + diagnostics: Arc<RwLock<DiagnosticCollection>>, + maybe_import_map: Arc<RwLock<Option<ImportMap>>>, + maybe_import_map_uri: Arc<RwLock<Option<Url>>>, +} + +#[derive(Debug, Clone, Default)] +pub struct StateSnapshot { + pub assets: Arc<RwLock<HashMap<ModuleSpecifier, Option<String>>>>, + pub doc_data: HashMap<ModuleSpecifier, DocumentData>, + pub file_cache: Arc<RwLock<MemoryCache>>, + pub sources: Arc<RwLock<Sources>>, +} + +impl LanguageServer { + pub fn new(client: Client) -> Self { + let maybe_custom_root = env::var("DENO_DIR").map(String::into).ok(); + let dir = deno_dir::DenoDir::new(maybe_custom_root) + .expect("could not access DENO_DIR"); + let location = dir.root.join("deps"); + let sources = Arc::new(RwLock::new(Sources::new(&location))); + + LanguageServer { + assets: Default::default(), + client, + ts_server: TsServer::new(), + config: Default::default(), + doc_data: Default::default(), + file_cache: Default::default(), + sources, + diagnostics: Default::default(), + maybe_import_map: Default::default(), + maybe_import_map_uri: Default::default(), + } + } + + pub async fn update_import_map(&self) -> Result<(), AnyError> { + let (maybe_import_map, maybe_root_uri) = { + let config = self.config.read().unwrap(); + (config.settings.import_map.clone(), config.root_uri.clone()) + }; + if let Some(import_map_str) = &maybe_import_map { + info!("update import map"); + let import_map_url = if let Ok(url) = Url::from_file_path(import_map_str) + { + Ok(url) + } else if let Some(root_uri) = &maybe_root_uri { + let root_path = root_uri + .to_file_path() + .map_err(|_| anyhow!("Bad root_uri: {}", root_uri))?; + let import_map_path = root_path.join(import_map_str); + Url::from_file_path(import_map_path).map_err(|_| { + anyhow!("Bad file path for import map: {:?}", import_map_str) + }) + } else { + Err(anyhow!( + "The path to the import map (\"{}\") is not resolvable.", + import_map_str + )) + }?; + let import_map_path = import_map_url + .to_file_path() + .map_err(|_| anyhow!("Bad file path."))?; + let import_map_json = + fs::read_to_string(import_map_path).await.map_err(|err| { + anyhow!( + "Failed to load the import map at: {}. [{}]", + import_map_url, + err + ) + })?; + let import_map = + ImportMap::from_json(&import_map_url.to_string(), &import_map_json)?; + *self.maybe_import_map_uri.write().unwrap() = Some(import_map_url); + *self.maybe_import_map.write().unwrap() = Some(import_map); + } else { + *self.maybe_import_map.write().unwrap() = None; + } + Ok(()) + } + + async fn prepare_diagnostics(&self) -> Result<(), AnyError> { + let (enabled, lint_enabled) = { + let config = self.config.read().unwrap(); + (config.settings.enable, config.settings.lint) + }; + + let lint = async { + if lint_enabled { + let diagnostic_collection = self.diagnostics.read().unwrap().clone(); + let diagnostics = diagnostics::generate_lint_diagnostics( + self.snapshot(), + diagnostic_collection, + ) + .await; + { + let mut diagnostics_collection = self.diagnostics.write().unwrap(); + for (file_id, version, diagnostics) in diagnostics { + diagnostics_collection.set( + file_id, + DiagnosticSource::Lint, + version, + diagnostics, + ); + } + } + self.publish_diagnostics().await? + }; + + Ok::<(), AnyError>(()) + }; + + let ts = async { + if enabled { + let diagnostics = { + let diagnostic_collection = self.diagnostics.read().unwrap().clone(); + diagnostics::generate_ts_diagnostics( + &self.ts_server, + &diagnostic_collection, + self.snapshot(), + ) + .await? + }; + { + let mut diagnostics_collection = self.diagnostics.write().unwrap(); + for (file_id, version, diagnostics) in diagnostics { + diagnostics_collection.set( + file_id, + DiagnosticSource::TypeScript, + version, + diagnostics, + ); + } + }; + self.publish_diagnostics().await? + } + + Ok::<(), AnyError>(()) + }; + + let (lint_res, ts_res) = tokio::join!(lint, ts); + lint_res?; + ts_res?; + + Ok(()) + } + + async fn publish_diagnostics(&self) -> Result<(), AnyError> { + let (maybe_changes, diagnostics_collection) = { + let mut diagnostics_collection = self.diagnostics.write().unwrap(); + let maybe_changes = diagnostics_collection.take_changes(); + (maybe_changes, diagnostics_collection.clone()) + }; + if let Some(diagnostic_changes) = maybe_changes { + let settings = self.config.read().unwrap().settings.clone(); + for file_id in diagnostic_changes { + // TODO(@kitsonk) not totally happy with the way we collect and store + // different types of diagnostics and offer them up to the client, we + // do need to send "empty" vectors though when a particular feature is + // disabled, otherwise the client will not clear down previous + // diagnostics + let mut diagnostics: Vec<Diagnostic> = if settings.lint { + diagnostics_collection + .diagnostics_for(file_id, DiagnosticSource::Lint) + .cloned() + .collect() + } else { + vec![] + }; + if settings.enable { + diagnostics.extend( + diagnostics_collection + .diagnostics_for(file_id, DiagnosticSource::TypeScript) + .cloned(), + ); + } + let specifier = { + let file_cache = self.file_cache.read().unwrap(); + file_cache.get_specifier(file_id).clone() + }; + let uri = specifier.as_url().clone(); + let version = if let Some(doc_data) = + self.doc_data.read().unwrap().get(&specifier) + { + doc_data.version + } else { + None + }; + self + .client + .publish_diagnostics(uri, diagnostics, version) + .await; + } + } + + Ok(()) + } + + pub fn snapshot(&self) -> StateSnapshot { + StateSnapshot { + assets: self.assets.clone(), + doc_data: self.doc_data.read().unwrap().clone(), + file_cache: self.file_cache.clone(), + sources: self.sources.clone(), + } + } + + pub async fn get_line_index( + &self, + specifier: ModuleSpecifier, + ) -> Result<Vec<u32>, AnyError> { + let line_index = if specifier.as_url().scheme() == "asset" { + let state_snapshot = self.snapshot(); + if let Some(source) = + tsc::get_asset(&specifier, &self.ts_server, &state_snapshot).await? + { + text::index_lines(&source) + } else { + return Err(anyhow!("asset source missing: {}", specifier)); + } + } else { + let file_cache = self.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 = self.sources.write().unwrap(); + if let Some(line_index) = sources.get_line_index(&specifier) { + line_index + } else { + return Err(anyhow!("source for specifier not found: {}", specifier)); + } + } + }; + Ok(line_index) + } +} + +#[lspower::async_trait] +impl lspower::LanguageServer for LanguageServer { + async fn initialize( + &self, + params: InitializeParams, + ) -> LSPResult<InitializeResult> { + info!("Starting Deno language server..."); + + let capabilities = capabilities::server_capabilities(¶ms.capabilities); + + let version = format!( + "{} ({}, {})", + crate::version::deno(), + env!("PROFILE"), + env!("TARGET") + ); + info!(" version: {}", version); + + let server_info = ServerInfo { + name: "deno-language-server".to_string(), + version: Some(version), + }; + + if let Some(client_info) = params.client_info { + info!( + "Connected to \"{}\" {}", + client_info.name, + client_info.version.unwrap_or_default(), + ); + } + + { + let mut config = self.config.write().unwrap(); + config.root_uri = params.root_uri; + if let Some(value) = params.initialization_options { + config.update(value)?; + } + config.update_capabilities(¶ms.capabilities); + } + + // 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", + })); + // TODO(lucacasonato): handle error correctly + self + .ts_server + .request(self.snapshot(), tsc::RequestMethod::Configure(ts_config)) + .await + .unwrap(); + + Ok(InitializeResult { + capabilities, + server_info: Some(server_info), + }) + } + + async fn initialized(&self, _: InitializedParams) { + // Check to see if we need to setup the import map + if let Err(err) = self.update_import_map().await { + self + .client + .show_message(MessageType::Warning, err.to_string()) + .await; + } + + // we are going to watch all the JSON files in the workspace, and the + // notification handler will pick up any of the changes of those files we + // are interested in. + let watch_registration_options = DidChangeWatchedFilesRegistrationOptions { + watchers: vec![FileSystemWatcher { + glob_pattern: "**/*.json".to_string(), + kind: Some(WatchKind::Change), + }], + }; + let registration = Registration { + id: "workspace/didChangeWatchedFiles".to_string(), + method: "workspace/didChangeWatchedFiles".to_string(), + register_options: Some( + serde_json::to_value(watch_registration_options).unwrap(), + ), + }; + if let Err(err) = self.client.register_capability(vec![registration]).await + { + warn!("Client errored on capabilities.\n{}", err); + } + + info!("Server ready."); + } + + async fn shutdown(&self) -> LSPResult<()> { + Ok(()) + } + + async fn did_open(&self, params: DidOpenTextDocumentParams) { + 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; + } + let specifier = utils::normalize_url(params.text_document.uri); + let maybe_import_map = self.maybe_import_map.read().unwrap().clone(); + if self + .doc_data + .write() + .unwrap() + .insert( + specifier.clone(), + DocumentData::new( + specifier.clone(), + params.text_document.version, + ¶ms.text_document.text, + maybe_import_map, + ), + ) + .is_some() + { + error!("duplicate DidOpenTextDocument: {}", specifier); + } + + self + .file_cache + .write() + .unwrap() + .set_contents(specifier, Some(params.text_document.text.into_bytes())); + // TODO(@lucacasonato): error handling + self.prepare_diagnostics().await.unwrap(); + } + + async fn did_change(&self, params: DidChangeTextDocumentParams) { + let specifier = utils::normalize_url(params.text_document.uri); + let mut content = { + let file_cache = self.file_cache.read().unwrap(); + let file_id = file_cache.lookup(&specifier).unwrap(); + file_cache.get_contents(file_id).unwrap() + }; + apply_content_changes(&mut content, params.content_changes); + { + let mut doc_data = self.doc_data.write().unwrap(); + let doc_data = doc_data.get_mut(&specifier).unwrap(); + let maybe_import_map = self.maybe_import_map.read().unwrap(); + doc_data.update( + params.text_document.version, + &content, + &maybe_import_map, + ); + } + + self + .file_cache + .write() + .unwrap() + .set_contents(specifier, Some(content.into_bytes())); + + // TODO(@lucacasonato): error handling + self.prepare_diagnostics().await.unwrap(); + } + + async fn did_close(&self, params: DidCloseTextDocumentParams) { + 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; + } + let specifier = utils::normalize_url(params.text_document.uri); + if self.doc_data.write().unwrap().remove(&specifier).is_none() { + error!("orphaned document: {}", specifier); + } + // TODO(@kitsonk) should we do garbage collection on the diagnostics? + // TODO(@lucacasonato): error handling + self.prepare_diagnostics().await.unwrap(); + } + + async fn did_save(&self, _params: DidSaveTextDocumentParams) { + // nothing to do yet... cleanup things? + } + + async fn did_change_configuration( + &self, + _params: DidChangeConfigurationParams, + ) { + let res = self + .client + .configuration(vec![ConfigurationItem { + scope_uri: None, + section: Some("deno".to_string()), + }]) + .await + .map(|vec| vec.get(0).cloned()); + + match res { + Err(err) => error!("failed to fetch the extension settings {:?}", err), + Ok(Some(config)) => { + if let Err(err) = self.config.write().unwrap().update(config) { + error!("failed to update settings: {}", err); + } + if let Err(err) = self.update_import_map().await { + self + .client + .show_message(MessageType::Warning, err.to_string()) + .await; + } + } + _ => error!("received empty extension settings from the client"), + } + } + + async fn did_change_watched_files( + &self, + params: DidChangeWatchedFilesParams, + ) { + // if the current import map has changed, we need to reload it + let maybe_import_map_uri = + self.maybe_import_map_uri.read().unwrap().clone(); + if let Some(import_map_uri) = maybe_import_map_uri { + if params.changes.iter().any(|fe| import_map_uri == fe.uri) { + if let Err(err) = self.update_import_map().await { + self + .client + .show_message(MessageType::Warning, err.to_string()) + .await; + } + } + } + } + + async fn formatting( + &self, + params: DocumentFormattingParams, + ) -> LSPResult<Option<Vec<TextEdit>>> { + let specifier = utils::normalize_url(params.text_document.uri.clone()); + let file_text = { + let file_cache = self.file_cache.read().unwrap(); + let file_id = file_cache.lookup(&specifier).unwrap(); + // TODO(lucacasonato): handle error properly + file_cache.get_contents(file_id).unwrap() + }; + + 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()) + }; + + // TODO(lucacasonato): handle error properly + let text_edits = tokio::task::spawn_blocking(move || { + let config = dprint::configuration::ConfigurationBuilder::new() + .deno() + .build(); + // TODO(@kitsonk) this could be handled better in `cli/tools/fmt.rs` in the + // future. + match dprint::format_text(&file_path, &file_text, &config) { + Ok(new_text) => Some(text::get_edits(&file_text, &new_text)), + Err(err) => { + warn!("Format error: {}", err); + None + } + } + }) + .await + .unwrap(); + + if let Some(text_edits) = text_edits { + if text_edits.is_empty() { + Ok(None) + } else { + Ok(Some(text_edits)) + } + } else { + Ok(None) + } + } + + async fn hover(&self, params: HoverParams) -> LSPResult<Option<Hover>> { + let specifier = utils::normalize_url( + params.text_document_position_params.text_document.uri, + ); + // TODO(lucacasonato): handle error correctly + let line_index = self.get_line_index(specifier.clone()).await.unwrap(); + let req = tsc::RequestMethod::GetQuickInfo(( + specifier, + text::to_char_pos( + &line_index, + params.text_document_position_params.position, + ), + )); + // TODO(lucacasonato): handle error correctly + let res = self.ts_server.request(self.snapshot(), req).await.unwrap(); + // TODO(lucacasonato): handle error correctly + let maybe_quick_info: Option<tsc::QuickInfo> = + serde_json::from_value(res).unwrap(); + if let Some(quick_info) = maybe_quick_info { + Ok(Some(quick_info.to_hover(&line_index))) + } else { + Ok(None) + } + } + + async fn document_highlight( + &self, + params: DocumentHighlightParams, + ) -> LSPResult<Option<Vec<DocumentHighlight>>> { + let specifier = utils::normalize_url( + params.text_document_position_params.text_document.uri, + ); + // TODO(lucacasonato): handle error correctly + let line_index = self.get_line_index(specifier.clone()).await.unwrap(); + let files_to_search = vec![specifier.clone()]; + let req = tsc::RequestMethod::GetDocumentHighlights(( + specifier, + text::to_char_pos( + &line_index, + params.text_document_position_params.position, + ), + files_to_search, + )); + // TODO(lucacasonato): handle error correctly + let res = self.ts_server.request(self.snapshot(), req).await.unwrap(); + // TODO(lucacasonato): handle error correctly + let maybe_document_highlights: Option<Vec<tsc::DocumentHighlights>> = + serde_json::from_value(res).unwrap(); + + 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) + } + } + + async fn references( + &self, + params: ReferenceParams, + ) -> LSPResult<Option<Vec<Location>>> { + let specifier = + utils::normalize_url(params.text_document_position.text_document.uri); + // TODO(lucacasonato): handle error correctly + let line_index = self.get_line_index(specifier.clone()).await.unwrap(); + let req = tsc::RequestMethod::GetReferences(( + specifier, + text::to_char_pos(&line_index, params.text_document_position.position), + )); + // TODO(lucacasonato): handle error correctly + let res = self.ts_server.request(self.snapshot(), req).await.unwrap(); + // TODO(lucacasonato): handle error correctly + let maybe_references: Option<Vec<tsc::ReferenceEntry>> = + serde_json::from_value(res).unwrap(); + + 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(); + // TODO(lucacasonato): handle error correctly + let line_index = + self.get_line_index(reference_specifier).await.unwrap(); + results.push(reference.to_location(&line_index)); + } + + Ok(Some(results)) + } else { + Ok(None) + } + } + + async fn goto_definition( + &self, + params: GotoDefinitionParams, + ) -> LSPResult<Option<GotoDefinitionResponse>> { + let specifier = utils::normalize_url( + params.text_document_position_params.text_document.uri, + ); + // TODO(lucacasonato): handle error correctly + let line_index = self.get_line_index(specifier.clone()).await.unwrap(); + let req = tsc::RequestMethod::GetDefinition(( + specifier, + text::to_char_pos( + &line_index, + params.text_document_position_params.position, + ), + )); + // TODO(lucacasonato): handle error correctly + let res = self.ts_server.request(self.snapshot(), req).await.unwrap(); + // TODO(lucacasonato): handle error correctly + let maybe_definition: Option<tsc::DefinitionInfoAndBoundSpan> = + serde_json::from_value(res).unwrap(); + + if let Some(definition) = maybe_definition { + Ok( + definition + .to_definition(&line_index, |s| self.get_line_index(s)) + .await, + ) + } else { + Ok(None) + } + } + + async fn completion( + &self, + params: CompletionParams, + ) -> LSPResult<Option<CompletionResponse>> { + let specifier = + utils::normalize_url(params.text_document_position.text_document.uri); + // TODO(lucacasonato): handle error correctly + let line_index = self.get_line_index(specifier.clone()).await.unwrap(); + let req = tsc::RequestMethod::GetCompletions(( + specifier, + text::to_char_pos(&line_index, params.text_document_position.position), + tsc::UserPreferences { + // TODO(lucacasonato): enable this. see https://github.com/denoland/deno/pull/8651 + include_completions_with_insert_text: Some(false), + ..Default::default() + }, + )); + // TODO(lucacasonato): handle error correctly + let res = self.ts_server.request(self.snapshot(), req).await.unwrap(); + // TODO(lucacasonato): handle error correctly + let maybe_completion_info: Option<tsc::CompletionInfo> = + serde_json::from_value(res).unwrap(); + + if let Some(completions) = maybe_completion_info { + Ok(Some(completions.into_completion_response(&line_index))) + } else { + Ok(None) + } + } + + async fn request_else( + &self, + method: &str, + params: Option<Value>, + ) -> LSPResult<Option<Value>> { + match method { + "deno/virtualTextDocument" => match params.map(serde_json::from_value) { + Some(Ok(params)) => Ok(Some( + serde_json::to_value(self.virtual_text_document(params).await?) + .map_err(|err| { + error!( + "Failed to serialize virtual_text_document response: {:#?}", + err + ); + LSPError::internal_error() + })?, + )), + Some(Err(err)) => Err(LSPError::invalid_params(err.to_string())), + None => Err(LSPError::invalid_params("Missing parameters")), + }, + _ => { + error!("Got a {} request, but no handler is defined", method); + Err(LSPError::method_not_found()) + } + } + } +} + +impl LanguageServer { + async fn virtual_text_document( + &self, + params: VirtualTextDocumentParams, + ) -> LSPResult<Option<String>> { + 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 = self.file_cache.read().unwrap(); + Some(format!( + r#"# Deno Language Server Status + + - Documents in memory: {} + + "#, + file_cache.len() + )) + } else { + match url.scheme() { + "asset" => { + let state_snapshot = self.snapshot(); + if let Some(text) = + tsc::get_asset(&specifier, &self.ts_server, &state_snapshot) + .await + .map_err(|_| LSPError::new(LSPErrorCode::InternalError))? + { + Some(text) + } else { + error!("Missing asset: {}", specifier); + None + } + } + _ => { + let mut sources = self.sources.write().unwrap(); + if let Some(text) = sources.get_text(&specifier) { + Some(text) + } else { + error!("The cached sources was not found: {}", specifier); + None + } + } + } + }; + Ok(contents) + } +} + +#[derive(Debug, Clone)] +pub struct DocumentData { + pub dependencies: Option<HashMap<String, analysis::Dependency>>, + pub version: Option<i32>, + specifier: ModuleSpecifier, +} + +impl DocumentData { + pub fn new( + specifier: ModuleSpecifier, + version: i32, + source: &str, + maybe_import_map: Option<ImportMap>, + ) -> Self { + let dependencies = if let Some((dependencies, _)) = + analysis::analyze_dependencies( + &specifier, + source, + &MediaType::from(&specifier), + &maybe_import_map, + ) { + Some(dependencies) + } else { + None + }; + Self { + dependencies, + version: Some(version), + specifier, + } + } + + pub fn update( + &mut self, + version: i32, + source: &str, + maybe_import_map: &Option<ImportMap>, + ) { + self.dependencies = if let Some((dependencies, _)) = + analysis::analyze_dependencies( + &self.specifier, + source, + &MediaType::from(&self.specifier), + maybe_import_map, + ) { + Some(dependencies) + } else { + None + }; + self.version = Some(version) + } +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VirtualTextDocumentParams { + pub text_document: TextDocumentIdentifier, +} + +#[cfg(test)] +mod tests { + use super::*; + use lspower::jsonrpc; + use lspower::ExitedError; + use lspower::LspService; + use std::fs; + use std::task::Poll; + use tower_test::mock::Spawn; + + enum LspResponse { + None, + RequestAny, + Request(u64, Value), + } + + struct LspTestHarness { + requests: Vec<(&'static str, LspResponse)>, + service: Spawn<LspService>, + } + + impl LspTestHarness { + pub fn new(requests: Vec<(&'static str, LspResponse)>) -> Self { + let (service, _) = LspService::new(LanguageServer::new); + let service = Spawn::new(service); + Self { requests, service } + } + + async fn run(&mut self) { + for (req_path_str, expected) in self.requests.iter() { + assert_eq!(self.service.poll_ready(), Poll::Ready(Ok(()))); + let fixtures_path = test_util::root_path().join("cli/tests/lsp"); + assert!(fixtures_path.is_dir()); + let req_path = fixtures_path.join(req_path_str); + let req_str = fs::read_to_string(req_path).unwrap(); + let req: jsonrpc::Incoming = serde_json::from_str(&req_str).unwrap(); + let response: Result<Option<jsonrpc::Outgoing>, ExitedError> = + self.service.call(req).await; + match response { + Ok(result) => match expected { + LspResponse::None => assert_eq!(result, None), + LspResponse::RequestAny => match result { + Some(jsonrpc::Outgoing::Response(_)) => (), + _ => panic!("unexpected result: {:?}", result), + }, + LspResponse::Request(id, value) => match result { + Some(jsonrpc::Outgoing::Response(resp)) => assert_eq!( + resp, + jsonrpc::Response::ok(jsonrpc::Id::Number(*id), value.clone()) + ), + _ => panic!("unexpected result: {:?}", result), + }, + }, + Err(err) => panic!("Error result: {}", err), + } + } + } + } + + #[tokio::test] + async fn test_startup_shutdown() { + let mut harness = LspTestHarness::new(vec![ + ("initialize_request.json", LspResponse::RequestAny), + ("initialized_notification.json", LspResponse::None), + ( + "shutdown_request.json", + LspResponse::Request(3, json!(null)), + ), + ("exit_notification.json", LspResponse::None), + ]); + harness.run().await; + } + + #[tokio::test] + async fn test_hover() { + let mut harness = LspTestHarness::new(vec![ + ("initialize_request.json", LspResponse::RequestAny), + ("initialized_notification.json", LspResponse::None), + ("did_open_notification.json", LspResponse::None), + ( + "hover_request.json", + LspResponse::Request( + 2, + json!({ + "contents": [ + { + "language": "typescript", + "value": "const Deno.args: string[]" + }, + "Returns the script arguments to the program. If for example we run a\nprogram:\n\ndeno run --allow-read https://deno.land/std/examples/cat.ts /etc/passwd\n\nThen `Deno.args` will contain:\n\n[ \"/etc/passwd\" ]" + ], + "range": { + "start": { + "line": 0, + "character": 17 + }, + "end": { + "line": 0, + "character": 21 + } + } + }), + ), + ), + ( + "shutdown_request.json", + LspResponse::Request(3, json!(null)), + ), + ("exit_notification.json", LspResponse::None), + ]); + harness.run().await; + } +} diff --git a/cli/lsp/lsp_extensions.rs b/cli/lsp/lsp_extensions.rs deleted file mode 100644 index eb0a62464..000000000 --- a/cli/lsp/lsp_extensions.rs +++ /dev/null @@ -1,26 +0,0 @@ -// 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 index 75c5bdb25..cfba1ecab 100644 --- a/cli/lsp/memory_cache.rs +++ b/cli/lsp/memory_cache.rs @@ -4,7 +4,6 @@ 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); @@ -111,10 +110,6 @@ impl MemoryCache { change_kind, }) } - - pub fn take_changes(&mut self) -> Vec<ChangedFile> { - mem::take(&mut self.changes) - } } impl fmt::Debug for MemoryCache { diff --git a/cli/lsp/mod.rs b/cli/lsp/mod.rs index 0f83e4ab2..912a8c684 100644 --- a/cli/lsp/mod.rs +++ b/cli/lsp/mod.rs @@ -1,472 +1,29 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use deno_core::error::AnyError; +use lspower::LspService; +use lspower::Server; mod analysis; mod capabilities; mod config; mod diagnostics; -mod dispatch; -mod handlers; -mod lsp_extensions; +mod language_server; 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::update_import_map; -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)?; +pub async fn start() -> Result<(), AnyError> { + let stdin = tokio::io::stdin(); + let stdout = tokio::io::stdout(); - connection.initialize_finish(initialize_id, initialize_result)?; + let (service, messages) = + LspService::new(language_server::LanguageServer::new); + Server::new(stdin, stdout) + .interleave(messages) + .serve(service) + .await; - 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(); - config.root_uri = initialize_params.root_uri.clone(); - 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); - - // 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", - })); - let state = server_state.snapshot(); - tsc::request( - &mut server_state.ts_runtime, - &state, - tsc::RequestMethod::Configure(ts_config), - )?; - - // listen for events and run the main loop - server_state.run(connection.receiver)?; - - io_threads.join()?; - info!("Stop language server"); Ok(()) } - -impl ServerState { - fn handle_event(&mut self, event: Event) -> Result<(), AnyError> { - let received = Instant::now(); - debug!("handle_event({:?})", event); - - match event { - Event::Message(message) => match message { - Message::Request(request) => self.on_request(request, received)?, - Message::Notification(notification) => { - self.on_notification(notification)? - } - Message::Response(response) => self.complete_request(response), - }, - Event::Task(mut task) => loop { - match task { - Task::Response(response) => self.respond(response), - Task::Diagnostics((source, diagnostics_per_file)) => { - for (file_id, version, diagnostics) in diagnostics_per_file { - self.diagnostics.set( - file_id, - source.clone(), - version, - diagnostics, - ); - } - } - } - - task = match self.task_receiver.try_recv() { - Ok(task) => task, - Err(_) => break, - }; - }, - } - - // process server sent notifications, like diagnostics - // TODO(@kitsonk) currently all of these refresh all open documents, though - // in a lot of cases, like linting, we would only care about the files - // themselves that have changed - if self.process_changes() { - debug!("process changes"); - let state = self.snapshot(); - self.spawn(move || { - let diagnostics = diagnostics::generate_linting_diagnostics(&state); - Task::Diagnostics((DiagnosticSource::Lint, diagnostics)) - }); - // TODO(@kitsonk) isolates do not have Send to be safely sent between - // threads, so I am not sure this is the best way to handle queuing up of - // getting the diagnostics from the isolate. - let state = self.snapshot(); - let diagnostics = - diagnostics::generate_ts_diagnostics(&state, &mut self.ts_runtime)?; - self.spawn(move || { - Task::Diagnostics((DiagnosticSource::TypeScript, diagnostics)) - }); - } - - // process any changes to the diagnostics - if let Some(diagnostic_changes) = self.diagnostics.take_changes() { - debug!("diagnostics have changed"); - let state = self.snapshot(); - for file_id in diagnostic_changes { - let file_cache = state.file_cache.read().unwrap(); - // TODO(@kitsonk) not totally happy with the way we collect and store - // different types of diagnostics and offer them up to the client, we - // do need to send "empty" vectors though when a particular feature is - // disabled, otherwise the client will not clear down previous - // diagnostics - let mut diagnostics: Vec<Diagnostic> = if state.config.settings.lint { - self - .diagnostics - .diagnostics_for(file_id, DiagnosticSource::Lint) - .cloned() - .collect() - } else { - vec![] - }; - if state.config.settings.enable { - diagnostics.extend( - self - .diagnostics - .diagnostics_for(file_id, DiagnosticSource::TypeScript) - .cloned(), - ); - } - let specifier = file_cache.get_specifier(file_id); - let uri = specifier.as_url().clone(); - let version = if let Some(doc_data) = self.doc_data.get(specifier) { - doc_data.version - } else { - None - }; - self.send_notification::<lsp_types::notification::PublishDiagnostics>( - lsp_types::PublishDiagnosticsParams { - uri, - diagnostics, - version, - }, - ); - } - } - - Ok(()) - } - - fn on_notification( - &mut self, - notification: Notification, - ) -> Result<(), AnyError> { - NotificationDispatcher { - notification: Some(notification), - server_state: self, - } - // TODO(@kitsonk) this is just stubbed out and we don't currently actually - // cancel in progress work, though most of our work isn't long running - .on::<lsp_types::notification::Cancel>(|state, params| { - let id: RequestId = match params.id { - lsp_types::NumberOrString::Number(id) => id.into(), - lsp_types::NumberOrString::String(id) => id.into(), - }; - state.cancel(id); - Ok(()) - })? - .on::<lsp_types::notification::DidOpenTextDocument>(|state, params| { - if params.text_document.uri.scheme() == "deno" { - // we can ignore virtual text documents opening, as they don't need to - // be tracked in memory, as they are static assets that won't change - // already managed by the language service - return Ok(()); - } - let specifier = utils::normalize_url(params.text_document.uri); - if state - .doc_data - .insert( - specifier.clone(), - DocumentData::new( - specifier.clone(), - params.text_document.version, - ¶ms.text_document.text, - state.maybe_import_map.clone(), - ), - ) - .is_some() - { - error!("duplicate DidOpenTextDocument: {}", specifier); - } - state - .file_cache - .write() - .unwrap() - .set_contents(specifier, Some(params.text_document.text.into_bytes())); - - Ok(()) - })? - .on::<lsp_types::notification::DidChangeTextDocument>(|state, params| { - let specifier = utils::normalize_url(params.text_document.uri); - let mut file_cache = state.file_cache.write().unwrap(); - let file_id = file_cache.lookup(&specifier).unwrap(); - let mut content = file_cache.get_contents(file_id)?; - apply_content_changes(&mut content, params.content_changes); - let doc_data = state.doc_data.get_mut(&specifier).unwrap(); - doc_data.update( - params.text_document.version, - &content, - state.maybe_import_map.clone(), - ); - file_cache.set_contents(specifier, Some(content.into_bytes())); - - Ok(()) - })? - .on::<lsp_types::notification::DidCloseTextDocument>(|state, params| { - if params.text_document.uri.scheme() == "deno" { - // we can ignore virtual text documents opening, as they don't need to - // be tracked in memory, as they are static assets that won't change - // already managed by the language service - return Ok(()); - } - let specifier = utils::normalize_url(params.text_document.uri); - if state.doc_data.remove(&specifier).is_none() { - error!("orphaned document: {}", specifier); - } - // TODO(@kitsonk) should we do garbage collection on the diagnostics? - - Ok(()) - })? - .on::<lsp_types::notification::DidSaveTextDocument>(|_state, _params| { - // nothing to do yet... cleanup things? - - Ok(()) - })? - .on::<lsp_types::notification::DidChangeConfiguration>(|state, _params| { - state.send_request::<lsp_types::request::WorkspaceConfiguration>( - lsp_types::ConfigurationParams { - items: vec![lsp_types::ConfigurationItem { - scope_uri: None, - section: Some("deno".to_string()), - }], - }, - |state, response| { - let Response { error, result, .. } = response; - - match (error, result) { - (Some(err), _) => { - error!("failed to fetch the extension settings: {:?}", err); - } - (None, Some(config)) => { - if let Some(config) = config.get(0) { - if let Err(err) = state.config.update(config.clone()) { - error!("failed to update settings: {}", err); - } - if let Err(err) = update_import_map(state) { - state - .send_notification::<lsp_types::notification::ShowMessage>( - lsp_types::ShowMessageParams { - typ: lsp_types::MessageType::Warning, - message: err.to_string(), - }, - ); - } - } - } - (None, None) => { - error!("received empty extension settings from the client"); - } - } - }, - ); - - Ok(()) - })? - .on::<lsp_types::notification::DidChangeWatchedFiles>(|state, params| { - // if the current import map has changed, we need to reload it - if let Some(import_map_uri) = &state.maybe_import_map_uri { - if params.changes.iter().any(|fe| import_map_uri == &fe.uri) { - update_import_map(state)?; - } - } - Ok(()) - })? - .finish(); - - Ok(()) - } - - fn on_request( - &mut self, - request: Request, - received: Instant, - ) -> Result<(), AnyError> { - self.register_request(&request, received); - - if self.shutdown_requested { - self.respond(Response::new_err( - request.id, - ErrorCode::InvalidRequest as i32, - "Shutdown already requested".to_string(), - )); - return Ok(()); - } - - if self.status == Status::Loading && request.method != "shutdown" { - self.respond(Response::new_err( - request.id, - ErrorCode::ContentModified as i32, - "Deno Language Server is still loading...".to_string(), - )); - return Ok(()); - } - - RequestDispatcher { - request: Some(request), - server_state: self, - } - .on_sync::<lsp_types::request::Shutdown>(|s, ()| { - s.shutdown_requested = true; - Ok(()) - })? - .on_sync::<lsp_types::request::DocumentHighlightRequest>( - handlers::handle_document_highlight, - )? - .on_sync::<lsp_types::request::GotoDefinition>( - handlers::handle_goto_definition, - )? - .on_sync::<lsp_types::request::HoverRequest>(handlers::handle_hover)? - .on_sync::<lsp_types::request::Completion>(handlers::handle_completion)? - .on_sync::<lsp_types::request::References>(handlers::handle_references)? - .on_sync::<lsp_extensions::VirtualTextDocument>( - handlers::handle_virtual_text_document, - )? - .on::<lsp_types::request::Formatting>(handlers::handle_formatting) - .finish(); - - Ok(()) - } - - /// Start consuming events from the provided receiver channel. - pub fn run(mut self, inbox: Receiver<Message>) -> Result<(), AnyError> { - // Check to see if we need to setup the import map - if let Err(err) = update_import_map(&mut self) { - self.send_notification::<lsp_types::notification::ShowMessage>( - lsp_types::ShowMessageParams { - typ: lsp_types::MessageType::Warning, - message: err.to_string(), - }, - ); - } - - // we are going to watch all the JSON files in the workspace, and the - // notification handler will pick up any of the changes of those files we - // are interested in. - let watch_registration_options = - lsp_types::DidChangeWatchedFilesRegistrationOptions { - watchers: vec![lsp_types::FileSystemWatcher { - glob_pattern: "**/*.json".to_string(), - kind: Some(lsp_types::WatchKind::Change), - }], - }; - let registration = lsp_types::Registration { - id: "workspace/didChangeWatchedFiles".to_string(), - method: "workspace/didChangeWatchedFiles".to_string(), - register_options: Some( - serde_json::to_value(watch_registration_options).unwrap(), - ), - }; - self.send_request::<lsp_types::request::RegisterCapability>( - lsp_types::RegistrationParams { - registrations: vec![registration], - }, - |_, _| (), - ); - - 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 index 09b0a4cc8..63b4ebd99 100644 --- a/cli/lsp/sources.rs +++ b/cli/lsp/sources.rs @@ -18,8 +18,6 @@ use std::collections::HashMap; use std::fs; use std::path::Path; use std::path::PathBuf; -use std::sync::Arc; -use std::sync::RwLock; use std::time::SystemTime; #[derive(Debug, Clone, Default)] @@ -34,7 +32,7 @@ struct Metadata { #[derive(Debug, Clone, Default)] pub struct Sources { http_cache: HttpCache, - maybe_import_map: Option<Arc<RwLock<ImportMap>>>, + maybe_import_map: Option<ImportMap>, metadata: HashMap<ModuleSpecifier, Metadata>, redirects: HashMap<ModuleSpecifier, ModuleSpecifier>, remotes: HashMap<ModuleSpecifier, PathBuf>, @@ -102,7 +100,7 @@ impl Sources { &specifier, &source, &media_type, - None, + &None, ) { maybe_types = mt; Some(dependencies) @@ -132,7 +130,7 @@ impl Sources { Some(analysis::resolve_import( types, &specifier, - self.maybe_import_map.clone(), + &self.maybe_import_map, )) } else { None @@ -142,7 +140,7 @@ impl Sources { &specifier, &source, &media_type, - None, + &None, ) { if maybe_types.is_none() { maybe_types = mt; diff --git a/cli/lsp/state.rs b/cli/lsp/state.rs deleted file mode 100644 index ceb4325a1..000000000 --- a/cli/lsp/state.rs +++ /dev/null @@ -1,395 +0,0 @@ -// 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::error::anyhow; -use deno_core::error::AnyError; -use deno_core::url::Url; -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::collections::HashMap; -use std::env; -use std::fmt; -use std::fs; -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 fn update_import_map(state: &mut ServerState) -> Result<(), AnyError> { - if let Some(import_map_str) = &state.config.settings.import_map { - let import_map_url = if let Ok(url) = Url::from_file_path(import_map_str) { - Ok(url) - } else if let Some(root_uri) = &state.config.root_uri { - let root_path = root_uri - .to_file_path() - .map_err(|_| anyhow!("Bad root_uri: {}", root_uri))?; - let import_map_path = root_path.join(import_map_str); - Url::from_file_path(import_map_path).map_err(|_| { - anyhow!("Bad file path for import map: {:?}", import_map_str) - }) - } else { - Err(anyhow!( - "The path to the import map (\"{}\") is not resolvable.", - import_map_str - )) - }?; - let import_map_path = import_map_url - .to_file_path() - .map_err(|_| anyhow!("Bad file path."))?; - let import_map_json = - fs::read_to_string(import_map_path).map_err(|err| { - anyhow!( - "Failed to load the import map at: {}. [{}]", - import_map_url, - err - ) - })?; - let import_map = - ImportMap::from_json(&import_map_url.to_string(), &import_map_json)?; - state.maybe_import_map_uri = Some(import_map_url); - state.maybe_import_map = Some(Arc::new(RwLock::new(import_map))); - } else { - state.maybe_import_map = None; - } - Ok(()) -} - -pub enum Event { - Message(Message), - Task(Task), -} - -impl fmt::Debug for Event { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let debug_verbose_not = - |notification: &Notification, f: &mut fmt::Formatter| { - f.debug_struct("Notification") - .field("method", ¬ification.method) - .finish() - }; - - match self { - Event::Message(Message::Notification(notification)) => { - if notification_is::<lsp_types::notification::DidOpenTextDocument>( - notification, - ) || notification_is::<lsp_types::notification::DidChangeTextDocument>( - notification, - ) { - return debug_verbose_not(notification, f); - } - } - Event::Task(Task::Response(response)) => { - return f - .debug_struct("Response") - .field("id", &response.id) - .field("error", &response.error) - .finish(); - } - _ => (), - } - match self { - Event::Message(it) => fmt::Debug::fmt(it, f), - Event::Task(it) => fmt::Debug::fmt(it, f), - } - } -} - -#[derive(Eq, PartialEq, Copy, Clone)] -pub enum Status { - Loading, - Ready, -} - -impl Default for Status { - fn default() -> Self { - Status::Loading - } -} - -#[derive(Debug)] -pub enum Task { - Diagnostics((DiagnosticSource, DiagnosticVec)), - Response(Response), -} - -#[derive(Debug, Clone)] -pub struct DocumentData { - pub dependencies: Option<HashMap<String, analysis::Dependency>>, - pub version: Option<i32>, - specifier: ModuleSpecifier, -} - -impl DocumentData { - pub fn new( - specifier: ModuleSpecifier, - version: i32, - source: &str, - maybe_import_map: Option<Arc<RwLock<ImportMap>>>, - ) -> Self { - let dependencies = if let Some((dependencies, _)) = - analysis::analyze_dependencies( - &specifier, - source, - &MediaType::from(&specifier), - maybe_import_map, - ) { - Some(dependencies) - } else { - None - }; - Self { - dependencies, - version: Some(version), - specifier, - } - } - - pub fn update( - &mut self, - version: i32, - source: &str, - maybe_import_map: Option<Arc<RwLock<ImportMap>>>, - ) { - self.dependencies = if let Some((dependencies, _)) = - analysis::analyze_dependencies( - &self.specifier, - source, - &MediaType::from(&self.specifier), - maybe_import_map, - ) { - Some(dependencies) - } else { - None - }; - self.version = Some(version) - } -} - -/// An immutable snapshot of the server state at a point in time. -#[derive(Debug, Clone, Default)] -pub struct ServerStateSnapshot { - pub assets: Arc<RwLock<HashMap<ModuleSpecifier, Option<String>>>>, - pub config: Config, - pub diagnostics: DiagnosticCollection, - pub doc_data: HashMap<ModuleSpecifier, DocumentData>, - pub file_cache: Arc<RwLock<MemoryCache>>, - pub sources: Arc<RwLock<Sources>>, -} - -pub struct ServerState { - pub assets: Arc<RwLock<HashMap<ModuleSpecifier, Option<String>>>>, - pub config: Config, - pub diagnostics: DiagnosticCollection, - pub doc_data: HashMap<ModuleSpecifier, DocumentData>, - pub file_cache: Arc<RwLock<MemoryCache>>, - pub maybe_import_map: Option<Arc<RwLock<ImportMap>>>, - pub maybe_import_map_uri: Option<Url>, - req_queue: ReqQueue, - sender: Sender<Message>, - pub sources: Arc<RwLock<Sources>>, - pub shutdown_requested: bool, - pub status: Status, - task_sender: Sender<Task>, - pub task_receiver: Receiver<Task>, - pub ts_runtime: JsRuntime, -} - -impl ServerState { - pub fn new(sender: Sender<Message>, config: Config) -> Self { - let (task_sender, task_receiver) = unbounded(); - let custom_root = env::var("DENO_DIR").map(String::into).ok(); - let dir = - deno_dir::DenoDir::new(custom_root).expect("could not access DENO_DIR"); - let location = dir.root.join("deps"); - let sources = Sources::new(&location); - // TODO(@kitsonk) we need to allow displaying diagnostics here, but the - // current compiler snapshot sends them to stdio which would totally break - // the language server... - let ts_runtime = tsc::start(false).expect("could not start tsc"); - - Self { - assets: Default::default(), - config, - diagnostics: Default::default(), - doc_data: Default::default(), - file_cache: Arc::new(RwLock::new(Default::default())), - maybe_import_map: None, - maybe_import_map_uri: None, - req_queue: Default::default(), - sender, - sources: Arc::new(RwLock::new(sources)), - shutdown_requested: false, - status: Default::default(), - task_receiver, - task_sender, - ts_runtime, - } - } - - pub fn cancel(&mut self, request_id: RequestId) { - if let Some(response) = self.req_queue.incoming.cancel(request_id) { - self.send(response.into()); - } - } - - pub fn complete_request(&mut self, response: Response) { - let handler = self.req_queue.outgoing.complete(response.id.clone()); - handler(self, response) - } - - pub fn next_event(&self, inbox: &Receiver<Message>) -> Option<Event> { - select! { - recv(inbox) -> msg => msg.ok().map(Event::Message), - recv(self.task_receiver) -> task => Some(Event::Task(task.unwrap())), - } - } - - /// Handle any changes and return a `bool` that indicates if there were - /// important changes to the state. - pub fn process_changes(&mut self) -> bool { - let mut file_cache = self.file_cache.write().unwrap(); - let changed_files = file_cache.take_changes(); - // other processing of changed files should be done here as needed - !changed_files.is_empty() - } - - pub fn register_request(&mut self, request: &Request, received: Instant) { - self - .req_queue - .incoming - .register(request.id.clone(), (request.method.clone(), received)); - } - - pub fn respond(&mut self, response: Response) { - if let Some((_, _)) = self.req_queue.incoming.complete(response.id.clone()) - { - self.send(response.into()); - } - } - - fn send(&mut self, message: Message) { - self.sender.send(message).unwrap() - } - - pub fn send_notification<N: lsp_types::notification::Notification>( - &mut self, - params: N::Params, - ) { - let notification = Notification::new(N::METHOD.to_string(), params); - self.send(notification.into()); - } - - pub fn send_request<R: lsp_types::request::Request>( - &mut self, - params: R::Params, - handler: ReqHandler, - ) { - let request = - self - .req_queue - .outgoing - .register(R::METHOD.to_string(), params, handler); - self.send(request.into()); - } - - pub fn snapshot(&self) -> ServerStateSnapshot { - ServerStateSnapshot { - assets: Arc::clone(&self.assets), - config: self.config.clone(), - diagnostics: self.diagnostics.clone(), - doc_data: self.doc_data.clone(), - file_cache: Arc::clone(&self.file_cache), - sources: Arc::clone(&self.sources), - } - } - - pub fn spawn<F>(&mut self, task: F) - where - F: FnOnce() -> Task + Send + 'static, - { - let sender = self.task_sender.clone(); - tokio::task::spawn_blocking(move || sender.send(task()).unwrap()); - } - - pub fn transition(&mut self, new_status: Status) { - self.status = new_status; - } -} - -#[cfg(test)] -mod tests { - use super::*; - use deno_core::serde_json::json; - use deno_core::serde_json::Value; - use lsp_server::Connection; - use tempfile::TempDir; - - #[test] - fn test_update_import_map() { - let temp_dir = TempDir::new().expect("could not create temp dir"); - let import_map_path = temp_dir.path().join("import_map.json"); - let import_map_str = &import_map_path.to_string_lossy(); - fs::write( - import_map_path.clone(), - r#"{ - "imports": { - "denoland/": "https://deno.land/x/" - } - }"#, - ) - .expect("could not write file"); - let mut config = Config::default(); - config - .update(json!({ - "enable": false, - "config": Value::Null, - "lint": false, - "importMap": import_map_str, - "unstable": true, - })) - .expect("could not update config"); - let (connection, _) = Connection::memory(); - let mut state = ServerState::new(connection.sender, config); - let result = update_import_map(&mut state); - assert!(result.is_ok()); - assert!(state.maybe_import_map.is_some()); - let expected = - Url::from_file_path(import_map_path).expect("could not parse url"); - assert_eq!(state.maybe_import_map_uri, Some(expected)); - let import_map = state.maybe_import_map.unwrap(); - let import_map = import_map.read().unwrap(); - assert_eq!( - import_map - .resolve("denoland/mod.ts", "https://example.com/index.js") - .expect("bad response"), - Some( - ModuleSpecifier::resolve_url("https://deno.land/x/mod.ts") - .expect("could not create URL") - ) - ); - } -} diff --git a/cli/lsp/text.rs b/cli/lsp/text.rs index 5bca534c1..a0bcb08d3 100644 --- a/cli/lsp/text.rs +++ b/cli/lsp/text.rs @@ -4,7 +4,8 @@ use deno_core::serde_json::json; use deno_core::serde_json::Value; use dissimilar::diff; use dissimilar::Chunk; -use lsp_types::TextEdit; +use lspower::lsp_types; +use lspower::lsp_types::TextEdit; use std::ops::Bound; use std::ops::Range; use std::ops::RangeBounds; diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 5cbf1ecc5..4cd13f70d 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -1,18 +1,21 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. use super::analysis::ResolvedImport; -use super::state::ServerStateSnapshot; +use super::language_server::StateSnapshot; use super::text; use super::utils; use crate::js; use crate::media_type::MediaType; +use crate::tokio_util::create_basic_runtime; use crate::tsc; use crate::tsc::ResolveArgs; use crate::tsc_config::TsConfig; +use deno_core::error::anyhow; use deno_core::error::custom_error; use deno_core::error::AnyError; +use deno_core::futures::Future; use deno_core::json_op_sync; use deno_core::serde::Deserialize; use deno_core::serde::Serialize; @@ -23,31 +26,89 @@ use deno_core::JsRuntime; use deno_core::ModuleSpecifier; use deno_core::OpFn; use deno_core::RuntimeOptions; +use lspower::lsp_types; use regex::Captures; use regex::Regex; use std::borrow::Cow; use std::collections::HashMap; +use std::thread; +use tokio::sync::mpsc; +use tokio::sync::oneshot; + +type Request = ( + RequestMethod, + StateSnapshot, + oneshot::Sender<Result<Value, AnyError>>, +); + +#[derive(Clone, Debug)] +pub struct TsServer(mpsc::UnboundedSender<Request>); + +impl TsServer { + pub fn new() -> Self { + let (tx, mut rx) = mpsc::unbounded_channel::<Request>(); + let _join_handle = thread::spawn(move || { + // 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 mut ts_runtime = start(false).expect("could not start tsc"); + + let mut runtime = create_basic_runtime(); + runtime.block_on(async { + while let Some((req, state_snapshot, tx)) = rx.recv().await { + let value = request(&mut ts_runtime, state_snapshot, req); + if tx.send(value).is_err() { + warn!("Unable to send result to client."); + } + } + }) + }); + + Self(tx) + } + + pub async fn request( + &self, + snapshot: StateSnapshot, + req: RequestMethod, + ) -> Result<Value, AnyError> { + let (tx, rx) = oneshot::channel::<Result<Value, AnyError>>(); + if self.0.send((req, snapshot, tx)).is_err() { + return Err(anyhow!("failed to send request to tsc thread")); + } + rx.await? + } +} /// Optionally returns an internal asset, first checking for any static assets /// in Rust, then checking any previously retrieved static assets from the /// isolate, and then finally, the tsc isolate itself. -pub fn get_asset( +pub async fn get_asset( specifier: &ModuleSpecifier, - runtime: &mut JsRuntime, - server_state: &ServerStateSnapshot, + ts_server: &TsServer, + state_snapshot: &StateSnapshot, ) -> Result<Option<String>, AnyError> { let specifier_str = specifier.to_string().replace("asset:///", ""); if let Some(asset_text) = tsc::get_asset(&specifier_str) { Ok(Some(asset_text.to_string())) } else { - let mut assets = server_state.assets.write().unwrap(); - if let Some(asset) = assets.get(specifier) { - Ok(asset.clone()) - } else { - let asset = request_asset(specifier, runtime, server_state)?; - assets.insert(specifier.clone(), asset.clone()); - Ok(asset) + { + let assets = state_snapshot.assets.read().unwrap(); + if let Some(asset) = assets.get(specifier) { + return Ok(asset.clone()); + } } + let asset: Option<String> = serde_json::from_value( + ts_server + .request( + state_snapshot.clone(), + RequestMethod::GetAsset(specifier.clone()), + ) + .await?, + )?; + let mut assets = state_snapshot.assets.write().unwrap(); + assets.insert(specifier.clone(), asset.clone()); + Ok(asset) } } @@ -235,7 +296,7 @@ pub enum ScriptElementKind { impl From<ScriptElementKind> for lsp_types::CompletionItemKind { fn from(kind: ScriptElementKind) -> Self { - use lsp_types::CompletionItemKind; + use lspower::lsp_types::CompletionItemKind; match kind { ScriptElementKind::PrimitiveType | ScriptElementKind::Keyword => { @@ -395,21 +456,21 @@ pub struct DefinitionInfoAndBoundSpan { } impl DefinitionInfoAndBoundSpan { - pub fn to_definition<F>( + pub async fn to_definition<F, Fut>( &self, line_index: &[u32], - mut index_provider: F, + index_provider: F, ) -> Option<lsp_types::GotoDefinitionResponse> where - F: FnMut(ModuleSpecifier) -> Vec<u32>, + F: Fn(ModuleSpecifier) -> Fut, + Fut: Future<Output = Result<Vec<u32>, AnyError>>, { 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 mut location_links = Vec::<lsp_types::LocationLink>::new(); + for di in definitions { + let target_specifier = + ModuleSpecifier::resolve_url(&di.file_name).unwrap(); + if let Ok(target_line_index) = index_provider(target_specifier).await { 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 { @@ -423,15 +484,14 @@ impl DefinitionInfoAndBoundSpan { di.text_span.to_range(&target_line_index), ) }; - lsp_types::LocationLink { + location_links.push(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 @@ -599,17 +659,17 @@ struct State<'a> { asset: Option<String>, last_id: usize, response: Option<Response>, - server_state: ServerStateSnapshot, + state_snapshot: StateSnapshot, snapshots: HashMap<(Cow<'a, str>, Cow<'a, str>), String>, } impl<'a> State<'a> { - fn new(server_state: ServerStateSnapshot) -> Self { + fn new(state_snapshot: StateSnapshot) -> Self { Self { asset: None, last_id: 1, response: None, - server_state, + state_snapshot, snapshots: Default::default(), } } @@ -626,9 +686,11 @@ fn cache_snapshot( .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)?; + let content = { + let file_cache = state.state_snapshot.file_cache.read().unwrap(); + let file_id = file_cache.lookup(&s).unwrap(); + file_cache.get_contents(file_id)? + }; state .snapshots .insert((specifier.into(), version.into()), content); @@ -713,7 +775,7 @@ fn get_change_range(state: &mut State, args: Value) -> Result<Value, AnyError> { fn get_length(state: &mut State, args: Value) -> Result<Value, AnyError> { let v: SourceSnapshotArgs = serde_json::from_value(args)?; let specifier = ModuleSpecifier::resolve_url(&v.specifier)?; - if state.server_state.doc_data.contains_key(&specifier) { + if state.state_snapshot.doc_data.contains_key(&specifier) { cache_snapshot(state, v.specifier.clone(), v.version.clone())?; let content = state .snapshots @@ -721,7 +783,7 @@ fn get_length(state: &mut State, args: Value) -> Result<Value, AnyError> { .unwrap(); Ok(json!(content.chars().count())) } else { - let mut sources = state.server_state.sources.write().unwrap(); + let mut sources = state.state_snapshot.sources.write().unwrap(); Ok(json!(sources.get_length(&specifier).unwrap())) } } @@ -738,7 +800,7 @@ struct GetTextArgs { fn get_text(state: &mut State, args: Value) -> Result<Value, AnyError> { let v: GetTextArgs = serde_json::from_value(args)?; let specifier = ModuleSpecifier::resolve_url(&v.specifier)?; - let content = if state.server_state.doc_data.contains_key(&specifier) { + let content = if state.state_snapshot.doc_data.contains_key(&specifier) { cache_snapshot(state, v.specifier.clone(), v.version.clone())?; state .snapshots @@ -746,7 +808,7 @@ fn get_text(state: &mut State, args: Value) -> Result<Value, AnyError> { .unwrap() .clone() } else { - let mut sources = state.server_state.sources.write().unwrap(); + let mut sources = state.state_snapshot.sources.write().unwrap(); sources.get_text(&specifier).unwrap() }; Ok(json!(text::slice(&content, v.start..v.end))) @@ -756,13 +818,13 @@ fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> { let v: ResolveArgs = serde_json::from_value(args)?; let mut resolved = Vec::<Option<(String, String)>>::new(); let referrer = ModuleSpecifier::resolve_url(&v.base)?; - let mut sources = if let Ok(sources) = state.server_state.sources.write() { + let mut sources = if let Ok(sources) = state.state_snapshot.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(doc_data) = state.state_snapshot.doc_data.get(&referrer) { if let Some(dependencies) = &doc_data.dependencies { for specifier in &v.specifiers { if specifier.starts_with("asset:///") { @@ -782,7 +844,7 @@ fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> { if let ResolvedImport::Resolved(resolved_specifier) = resolved_import { if state - .server_state + .state_snapshot .doc_data .contains_key(&resolved_specifier) || sources.contains(&resolved_specifier) @@ -837,7 +899,7 @@ fn respond(state: &mut State, args: Value) -> Result<Value, AnyError> { fn script_names(state: &mut State, _args: Value) -> Result<Value, AnyError> { let script_names: Vec<&ModuleSpecifier> = - state.server_state.doc_data.keys().collect(); + state.state_snapshot.doc_data.keys().collect(); Ok(json!(script_names)) } @@ -850,13 +912,13 @@ struct ScriptVersionArgs { fn script_version(state: &mut State, args: Value) -> Result<Value, AnyError> { let v: ScriptVersionArgs = serde_json::from_value(args)?; let specifier = ModuleSpecifier::resolve_url(&v.specifier)?; - let maybe_doc_data = state.server_state.doc_data.get(&specifier); + let maybe_doc_data = state.state_snapshot.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(); + let mut sources = state.state_snapshot.sources.write().unwrap(); if let Some(version) = sources.get_script_version(&specifier) { return Ok(json!(version)); } @@ -889,7 +951,7 @@ pub fn start(debug: bool) -> Result<JsRuntime, AnyError> { { let op_state = runtime.op_state(); let mut op_state = op_state.borrow_mut(); - op_state.put(State::new(ServerStateSnapshot::default())); + op_state.put(State::new(StateSnapshot::default())); } runtime.register_op("op_dispose", op(dispose)); @@ -1071,14 +1133,14 @@ impl RequestMethod { /// Send a request into a runtime and return the JSON value of the response. pub fn request( runtime: &mut JsRuntime, - server_state: &ServerStateSnapshot, + state_snapshot: StateSnapshot, method: RequestMethod, ) -> Result<Value, AnyError> { let id = { let op_state = runtime.op_state(); let mut op_state = op_state.borrow_mut(); let state = op_state.borrow_mut::<State>(); - state.server_state = server_state.clone(); + state.state_snapshot = state_snapshot; state.last_id += 1; state.last_id }; @@ -1101,40 +1163,16 @@ pub fn request( } } -fn request_asset( - specifier: &ModuleSpecifier, - runtime: &mut JsRuntime, - server_state: &ServerStateSnapshot, -) -> Result<Option<String>, AnyError> { - let id = { - let op_state = runtime.op_state(); - let mut op_state = op_state.borrow_mut(); - let state = op_state.borrow_mut::<State>(); - state.server_state = server_state.clone(); - state.last_id += 1; - state.last_id - }; - let request_params = RequestMethod::GetAsset(specifier.clone()).to_value(id); - let request_src = format!("globalThis.serverRequest({});", request_params); - runtime.execute("[native_code]", &request_src)?; - - let op_state = runtime.op_state(); - let mut op_state = op_state.borrow_mut(); - let state = op_state.borrow_mut::<State>(); - - Ok(state.asset.clone()) -} - #[cfg(test)] mod tests { use super::super::memory_cache::MemoryCache; - use super::super::state::DocumentData; use super::*; + use crate::lsp::language_server::DocumentData; use std::collections::HashMap; use std::sync::Arc; use std::sync::RwLock; - fn mock_server_state(sources: Vec<(&str, &str, i32)>) -> ServerStateSnapshot { + fn mock_state_snapshot(sources: Vec<(&str, &str, i32)>) -> StateSnapshot { let mut doc_data = HashMap::new(); let mut file_cache = MemoryCache::default(); for (specifier, content, version) in sources { @@ -1147,10 +1185,8 @@ mod tests { file_cache.set_contents(specifier, Some(content.as_bytes().to_vec())); } let file_cache = Arc::new(RwLock::new(file_cache)); - ServerStateSnapshot { + StateSnapshot { assets: Default::default(), - config: Default::default(), - diagnostics: Default::default(), doc_data, file_cache, sources: Default::default(), @@ -1161,20 +1197,20 @@ mod tests { debug: bool, config: Value, sources: Vec<(&str, &str, i32)>, - ) -> (JsRuntime, ServerStateSnapshot) { - let server_state = mock_server_state(sources.clone()); + ) -> (JsRuntime, StateSnapshot) { + let state_snapshot = mock_state_snapshot(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, + state_snapshot.clone(), RequestMethod::Configure(ts_config) ) .expect("failed request"), json!(true) ); - (runtime, server_state) + (runtime, state_snapshot) } #[test] @@ -1207,7 +1243,7 @@ mod tests { #[test] fn test_project_reconfigure() { - let (mut runtime, server_state) = setup( + let (mut runtime, state_snapshot) = setup( false, json!({ "target": "esnext", @@ -1224,7 +1260,7 @@ mod tests { })); let result = request( &mut runtime, - &server_state, + state_snapshot, RequestMethod::Configure(ts_config), ); assert!(result.is_ok()); @@ -1234,7 +1270,7 @@ mod tests { #[test] fn test_get_semantic_diagnostics() { - let (mut runtime, server_state) = setup( + let (mut runtime, state_snapshot) = setup( false, json!({ "target": "esnext", @@ -1247,7 +1283,7 @@ mod tests { .expect("could not resolve url"); let result = request( &mut runtime, - &server_state, + state_snapshot, RequestMethod::GetSemanticDiagnostics(specifier), ); assert!(result.is_ok()); @@ -1276,7 +1312,7 @@ mod tests { #[test] fn test_module_resolution() { - let (mut runtime, server_state) = setup( + let (mut runtime, state_snapshot) = setup( false, json!({ "target": "esnext", @@ -1300,7 +1336,7 @@ mod tests { .expect("could not resolve url"); let result = request( &mut runtime, - &server_state, + state_snapshot, RequestMethod::GetSemanticDiagnostics(specifier), ); assert!(result.is_ok()); @@ -1310,7 +1346,7 @@ mod tests { #[test] fn test_bad_module_specifiers() { - let (mut runtime, server_state) = setup( + let (mut runtime, state_snapshot) = setup( false, json!({ "target": "esnext", @@ -1330,7 +1366,7 @@ mod tests { .expect("could not resolve url"); let result = request( &mut runtime, - &server_state, + state_snapshot, RequestMethod::GetSyntacticDiagnostics(specifier), ); assert!(result.is_ok()); @@ -1340,7 +1376,7 @@ mod tests { #[test] fn test_remote_modules() { - let (mut runtime, server_state) = setup( + let (mut runtime, state_snapshot) = setup( false, json!({ "target": "esnext", @@ -1364,7 +1400,7 @@ mod tests { .expect("could not resolve url"); let result = request( &mut runtime, - &server_state, + state_snapshot, RequestMethod::GetSyntacticDiagnostics(specifier), ); assert!(result.is_ok()); @@ -1374,7 +1410,7 @@ mod tests { #[test] fn test_partial_modules() { - let (mut runtime, server_state) = setup( + let (mut runtime, state_snapshot) = setup( false, json!({ "target": "esnext", @@ -1401,7 +1437,7 @@ mod tests { .expect("could not resolve url"); let result = request( &mut runtime, - &server_state, + state_snapshot, RequestMethod::GetSyntacticDiagnostics(specifier), ); assert!(result.is_ok()); @@ -1428,7 +1464,7 @@ mod tests { #[test] fn test_request_asset() { - let (mut runtime, server_state) = setup( + let (mut runtime, state_snapshot) = setup( false, json!({ "target": "esnext", @@ -1440,9 +1476,14 @@ mod tests { ); let specifier = ModuleSpecifier::resolve_url("asset:///lib.esnext.d.ts") .expect("could not resolve url"); - let result = request_asset(&specifier, &mut runtime, &server_state); + let result = request( + &mut runtime, + state_snapshot, + RequestMethod::GetAsset(specifier), + ); assert!(result.is_ok()); - let response = result.unwrap(); + let response: Option<String> = + serde_json::from_value(result.unwrap()).unwrap(); assert!(response.is_some()); } } diff --git a/cli/lsp/utils.rs b/cli/lsp/utils.rs index 0c3d5a635..3bdd00875 100644 --- a/cli/lsp/utils.rs +++ b/cli/lsp/utils.rs @@ -1,71 +1,9 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -use deno_core::error::custom_error; use deno_core::error::AnyError; -use deno_core::serde_json::Value; use deno_core::url::Position; use deno_core::url::Url; use deno_core::ModuleSpecifier; -use lsp_server::Notification; -use serde::de::DeserializeOwned; -use std::error::Error; -use std::fmt; - -// TODO(@kitsonk) support actually supporting cancellation requests from the -// client. - -pub struct Canceled { - _private: (), -} - -impl Canceled { - #[allow(unused)] - pub fn new() -> Self { - Self { _private: () } - } - - #[allow(unused)] - pub fn throw() -> ! { - std::panic::resume_unwind(Box::new(Canceled::new())) - } -} - -impl fmt::Display for Canceled { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "cancelled") - } -} - -impl fmt::Debug for Canceled { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Canceled") - } -} - -impl Error for Canceled {} - -pub fn from_json<T: DeserializeOwned>( - what: &'static str, - json: Value, -) -> Result<T, AnyError> { - let response = T::deserialize(&json).map_err(|err| { - custom_error( - "DeserializeFailed", - format!("Failed to deserialize {}: {}; {}", what, err, json), - ) - })?; - Ok(response) -} - -pub fn is_canceled(e: &(dyn Error + 'static)) -> bool { - e.downcast_ref::<Canceled>().is_some() -} - -pub fn notification_is<N: lsp_types::notification::Notification>( - notification: &Notification, -) -> bool { - notification.method == N::METHOD -} /// Normalizes a file name returned from the TypeScript compiler into a URI that /// should be sent by the language server to the client. diff --git a/cli/main.rs b/cli/main.rs index cd682498e..55f1ac9ce 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -416,7 +416,7 @@ async fn install_command( } async fn language_server_command() -> Result<(), AnyError> { - lsp::start() + lsp::start().await } async fn lint_command( diff --git a/cli/tests/lsp_tests.rs b/cli/tests/lsp_tests.rs deleted file mode 100644 index 7de655ac8..000000000 --- a/cli/tests/lsp_tests.rs +++ /dev/null @@ -1,88 +0,0 @@ -// 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/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index 0be0fdc2c..9b08dee93 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -492,10 +492,7 @@ delete Object.prototype.__proto__; request.specifier, ts.ScriptTarget.ESNext, ); - return core.jsonOpSync( - "op_set_asset", - { text: sourceFile && sourceFile.text }, - ); + return respond(id, sourceFile && sourceFile.text); } case "getSemanticDiagnostics": { const diagnostics = languageService.getSemanticDiagnostics( |