summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock163
-rw-r--r--cli/Cargo.toml5
-rw-r--r--cli/lsp/README.md17
-rw-r--r--cli/lsp/analysis.rs31
-rw-r--r--cli/lsp/capabilities.rs26
-rw-r--r--cli/lsp/config.rs9
-rw-r--r--cli/lsp/diagnostics.rs201
-rw-r--r--cli/lsp/dispatch.rs185
-rw-r--r--cli/lsp/handlers.rs304
-rw-r--r--cli/lsp/language_server.rs981
-rw-r--r--cli/lsp/lsp_extensions.rs26
-rw-r--r--cli/lsp/memory_cache.rs5
-rw-r--r--cli/lsp/mod.rs469
-rw-r--r--cli/lsp/sources.rs10
-rw-r--r--cli/lsp/state.rs395
-rw-r--r--cli/lsp/text.rs3
-rw-r--r--cli/lsp/tsc.rs227
-rw-r--r--cli/lsp/utils.rs62
-rw-r--r--cli/main.rs2
-rw-r--r--cli/tests/lsp_tests.rs88
-rw-r--r--cli/tsc/99_main_compiler.js5
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(&params.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(&params.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,
+ &params.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,
- &params.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", &notification.method)
- .finish()
- };
-
- match self {
- Event::Message(Message::Notification(notification)) => {
- if notification_is::<lsp_types::notification::DidOpenTextDocument>(
- notification,
- ) || notification_is::<lsp_types::notification::DidChangeTextDocument>(
- notification,
- ) {
- return debug_verbose_not(notification, f);
- }
- }
- Event::Task(Task::Response(response)) => {
- return f
- .debug_struct("Response")
- .field("id", &response.id)
- .field("error", &response.error)
- .finish();
- }
- _ => (),
- }
- match self {
- Event::Message(it) => fmt::Debug::fmt(it, f),
- Event::Task(it) => fmt::Debug::fmt(it, f),
- }
- }
-}
-
-#[derive(Eq, PartialEq, Copy, Clone)]
-pub enum Status {
- Loading,
- Ready,
-}
-
-impl Default for Status {
- fn default() -> Self {
- Status::Loading
- }
-}
-
-#[derive(Debug)]
-pub enum Task {
- Diagnostics((DiagnosticSource, DiagnosticVec)),
- Response(Response),
-}
-
-#[derive(Debug, Clone)]
-pub struct DocumentData {
- pub dependencies: Option<HashMap<String, analysis::Dependency>>,
- pub version: Option<i32>,
- specifier: ModuleSpecifier,
-}
-
-impl DocumentData {
- pub fn new(
- specifier: ModuleSpecifier,
- version: i32,
- source: &str,
- maybe_import_map: Option<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(