1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-26 00:59:24 -05:00

refactor: rewrite lsp to be async (#8727)

Co-authored-by: Luca Casonato <lucacasonato@yahoo.com>
This commit is contained in:
Luca Casonato 2020-12-21 14:44:26 +01:00 committed by GitHub
parent 3078fcf55a
commit bd85d0ed42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1399 additions and 1819 deletions

167
Cargo.lock generated
View file

@ -110,6 +110,17 @@ dependencies = [
"pin-project-lite 0.1.7",
]
[[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"
@ -121,6 +132,18 @@ dependencies = [
"winapi 0.3.9",
]
[[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"
@ -366,20 +389,10 @@ 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"
@ -391,17 +404,6 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d"
dependencies = [
"autocfg 1.0.1",
"cfg-if 1.0.0",
"lazy_static",
]
[[package]]
name = "darling"
version = "0.10.2"
@ -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",
@ -1100,6 +1101,15 @@ dependencies = [
"http",
]
[[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"
@ -1376,23 +1386,11 @@ dependencies = [
"cfg-if 0.1.10",
]
[[package]]
name = "lsp-server"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69b18dfe0e4a380b872aa79d8e0ee6c3d7a9682466e84b83ad807c88b3545f79"
dependencies = [
"crossbeam-channel 0.5.0",
"log",
"serde",
"serde_json",
]
[[package]]
name = "lsp-types"
version = "0.84.0"
version = "0.85.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b95be71fe205e44de754185bcf86447b65813ce1ceb298f8d3793ade5fff08d"
checksum = "857650f3e83fb62f89d15410414e0ed7d0735445020da398d37f65d20a5423b9"
dependencies = [
"base64 0.12.3",
"bitflags",
@ -1402,6 +1400,40 @@ dependencies = [
"url",
]
[[package]]
name = "lspower"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64106b17ca8f6f73cc21a3d1f39684ff65293a291aa96026aee85eaae02339a5"
dependencies = [
"async-trait",
"auto_impl",
"bytes 0.5.6",
"dashmap",
"futures",
"log",
"lsp-types",
"lspower-macros",
"nom",
"serde",
"serde_json",
"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]]
name = "matches"
version = "0.1.8"
@ -1601,7 +1633,7 @@ checksum = "77d03607cf88b4b160ba0e9ed425fff3cee3b55ac813f0c685b3a3772da37d0e"
dependencies = [
"anymap",
"bitflags",
"crossbeam-channel 0.4.4",
"crossbeam-channel",
"filetime",
"fsevent",
"fsevent-sys",
@ -1876,6 +1908,30 @@ version = "0.1.1"
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"
@ -3036,6 +3092,17 @@ dependencies = [
"webpki",
]
[[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"
@ -3072,12 +3139,32 @@ dependencies = [
"serde",
]
[[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"

View file

@ -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

View file

@ -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.

View file

@ -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());

View file

@ -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,
}
}

View file

@ -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(())
}

View file

@ -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
}
}
}
}
diagnostics
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(),
}
}
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));
}
}

View file

@ -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
}
}
}
}

View file

@ -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)
}

981
cli/lsp/language_server.rs Normal file
View file

@ -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;
}
}

View file

@ -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";
}

View file

@ -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 {

View file

@ -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;
pub async fn start() -> Result<(), AnyError> {
let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout();
use crate::tsc_config::TsConfig;
let (service, messages) =
LspService::new(language_server::LanguageServer::new);
Server::new(stdin, stdout)
.interleave(messages)
.serve(service)
.await;
use crossbeam_channel::Receiver;
use deno_core::error::custom_error;
use deno_core::error::AnyError;
use deno_core::serde_json;
use deno_core::serde_json::json;
use lsp_server::Connection;
use lsp_server::ErrorCode;
use lsp_server::Message;
use lsp_server::Notification;
use lsp_server::Request;
use lsp_server::RequestId;
use lsp_server::Response;
use lsp_types::notification::Notification as _;
use lsp_types::Diagnostic;
use lsp_types::InitializeParams;
use lsp_types::InitializeResult;
use lsp_types::ServerInfo;
use std::env;
use std::time::Instant;
pub fn start() -> Result<(), AnyError> {
info!("Starting Deno language server...");
let (connection, io_threads) = Connection::stdio();
let (initialize_id, initialize_params) = connection.initialize_start()?;
let initialize_params: InitializeParams =
serde_json::from_value(initialize_params)?;
let capabilities =
capabilities::server_capabilities(&initialize_params.capabilities);
let version = format!(
"{} ({}, {})",
crate::version::deno(),
env!("PROFILE"),
env!("TARGET")
);
info!(" version: {}", version);
let initialize_result = InitializeResult {
capabilities,
server_info: Some(ServerInfo {
name: "deno-language-server".to_string(),
version: Some(version),
}),
};
let initialize_result = serde_json::to_value(initialize_result)?;
connection.initialize_finish(initialize_id, initialize_result)?;
if let Some(client_info) = initialize_params.client_info {
info!(
"Connected to \"{}\" {}",
client_info.name,
client_info.version.unwrap_or_default()
);
}
let mut config = Config::default();
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.",
))
}
}

View file

@ -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;

View file

@ -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")
)
);
}
}

View file

@ -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;

View file

@ -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());
}
}

View file

@ -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.

View file

@ -416,7 +416,7 @@ async fn install_command(
}
async fn language_server_command() -> Result<(), AnyError> {
lsp::start()
lsp::start().await
}
async fn lint_command(

View file

@ -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"));
}

View file

@ -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(