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:
parent
3078fcf55a
commit
bd85d0ed42
21 changed files with 1399 additions and 1819 deletions
167
Cargo.lock
generated
167
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
981
cli/lsp/language_server.rs
Normal 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(¶ms.capabilities);
|
||||
|
||||
let version = format!(
|
||||
"{} ({}, {})",
|
||||
crate::version::deno(),
|
||||
env!("PROFILE"),
|
||||
env!("TARGET")
|
||||
);
|
||||
info!(" version: {}", version);
|
||||
|
||||
let server_info = ServerInfo {
|
||||
name: "deno-language-server".to_string(),
|
||||
version: Some(version),
|
||||
};
|
||||
|
||||
if let Some(client_info) = params.client_info {
|
||||
info!(
|
||||
"Connected to \"{}\" {}",
|
||||
client_info.name,
|
||||
client_info.version.unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let mut config = self.config.write().unwrap();
|
||||
config.root_uri = params.root_uri;
|
||||
if let Some(value) = params.initialization_options {
|
||||
config.update(value)?;
|
||||
}
|
||||
config.update_capabilities(¶ms.capabilities);
|
||||
}
|
||||
|
||||
// TODO(@kitsonk) need to make this configurable, respect unstable
|
||||
let ts_config = TsConfig::new(json!({
|
||||
"allowJs": true,
|
||||
"experimentalDecorators": true,
|
||||
"isolatedModules": true,
|
||||
"lib": ["deno.ns", "deno.window"],
|
||||
"module": "esnext",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"target": "esnext",
|
||||
}));
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
self
|
||||
.ts_server
|
||||
.request(self.snapshot(), tsc::RequestMethod::Configure(ts_config))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(InitializeResult {
|
||||
capabilities,
|
||||
server_info: Some(server_info),
|
||||
})
|
||||
}
|
||||
|
||||
async fn initialized(&self, _: InitializedParams) {
|
||||
// Check to see if we need to setup the import map
|
||||
if let Err(err) = self.update_import_map().await {
|
||||
self
|
||||
.client
|
||||
.show_message(MessageType::Warning, err.to_string())
|
||||
.await;
|
||||
}
|
||||
|
||||
// we are going to watch all the JSON files in the workspace, and the
|
||||
// notification handler will pick up any of the changes of those files we
|
||||
// are interested in.
|
||||
let watch_registration_options = DidChangeWatchedFilesRegistrationOptions {
|
||||
watchers: vec![FileSystemWatcher {
|
||||
glob_pattern: "**/*.json".to_string(),
|
||||
kind: Some(WatchKind::Change),
|
||||
}],
|
||||
};
|
||||
let registration = Registration {
|
||||
id: "workspace/didChangeWatchedFiles".to_string(),
|
||||
method: "workspace/didChangeWatchedFiles".to_string(),
|
||||
register_options: Some(
|
||||
serde_json::to_value(watch_registration_options).unwrap(),
|
||||
),
|
||||
};
|
||||
if let Err(err) = self.client.register_capability(vec![registration]).await
|
||||
{
|
||||
warn!("Client errored on capabilities.\n{}", err);
|
||||
}
|
||||
|
||||
info!("Server ready.");
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> LSPResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn did_open(&self, params: DidOpenTextDocumentParams) {
|
||||
if params.text_document.uri.scheme() == "deno" {
|
||||
// we can ignore virtual text documents opening, as they don't need to
|
||||
// be tracked in memory, as they are static assets that won't change
|
||||
// already managed by the language service
|
||||
return;
|
||||
}
|
||||
let specifier = utils::normalize_url(params.text_document.uri);
|
||||
let maybe_import_map = self.maybe_import_map.read().unwrap().clone();
|
||||
if self
|
||||
.doc_data
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(
|
||||
specifier.clone(),
|
||||
DocumentData::new(
|
||||
specifier.clone(),
|
||||
params.text_document.version,
|
||||
¶ms.text_document.text,
|
||||
maybe_import_map,
|
||||
),
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
error!("duplicate DidOpenTextDocument: {}", specifier);
|
||||
}
|
||||
|
||||
self
|
||||
.file_cache
|
||||
.write()
|
||||
.unwrap()
|
||||
.set_contents(specifier, Some(params.text_document.text.into_bytes()));
|
||||
// TODO(@lucacasonato): error handling
|
||||
self.prepare_diagnostics().await.unwrap();
|
||||
}
|
||||
|
||||
async fn did_change(&self, params: DidChangeTextDocumentParams) {
|
||||
let specifier = utils::normalize_url(params.text_document.uri);
|
||||
let mut content = {
|
||||
let file_cache = self.file_cache.read().unwrap();
|
||||
let file_id = file_cache.lookup(&specifier).unwrap();
|
||||
file_cache.get_contents(file_id).unwrap()
|
||||
};
|
||||
apply_content_changes(&mut content, params.content_changes);
|
||||
{
|
||||
let mut doc_data = self.doc_data.write().unwrap();
|
||||
let doc_data = doc_data.get_mut(&specifier).unwrap();
|
||||
let maybe_import_map = self.maybe_import_map.read().unwrap();
|
||||
doc_data.update(
|
||||
params.text_document.version,
|
||||
&content,
|
||||
&maybe_import_map,
|
||||
);
|
||||
}
|
||||
|
||||
self
|
||||
.file_cache
|
||||
.write()
|
||||
.unwrap()
|
||||
.set_contents(specifier, Some(content.into_bytes()));
|
||||
|
||||
// TODO(@lucacasonato): error handling
|
||||
self.prepare_diagnostics().await.unwrap();
|
||||
}
|
||||
|
||||
async fn did_close(&self, params: DidCloseTextDocumentParams) {
|
||||
if params.text_document.uri.scheme() == "deno" {
|
||||
// we can ignore virtual text documents opening, as they don't need to
|
||||
// be tracked in memory, as they are static assets that won't change
|
||||
// already managed by the language service
|
||||
return;
|
||||
}
|
||||
let specifier = utils::normalize_url(params.text_document.uri);
|
||||
if self.doc_data.write().unwrap().remove(&specifier).is_none() {
|
||||
error!("orphaned document: {}", specifier);
|
||||
}
|
||||
// TODO(@kitsonk) should we do garbage collection on the diagnostics?
|
||||
// TODO(@lucacasonato): error handling
|
||||
self.prepare_diagnostics().await.unwrap();
|
||||
}
|
||||
|
||||
async fn did_save(&self, _params: DidSaveTextDocumentParams) {
|
||||
// nothing to do yet... cleanup things?
|
||||
}
|
||||
|
||||
async fn did_change_configuration(
|
||||
&self,
|
||||
_params: DidChangeConfigurationParams,
|
||||
) {
|
||||
let res = self
|
||||
.client
|
||||
.configuration(vec![ConfigurationItem {
|
||||
scope_uri: None,
|
||||
section: Some("deno".to_string()),
|
||||
}])
|
||||
.await
|
||||
.map(|vec| vec.get(0).cloned());
|
||||
|
||||
match res {
|
||||
Err(err) => error!("failed to fetch the extension settings {:?}", err),
|
||||
Ok(Some(config)) => {
|
||||
if let Err(err) = self.config.write().unwrap().update(config) {
|
||||
error!("failed to update settings: {}", err);
|
||||
}
|
||||
if let Err(err) = self.update_import_map().await {
|
||||
self
|
||||
.client
|
||||
.show_message(MessageType::Warning, err.to_string())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
_ => error!("received empty extension settings from the client"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn did_change_watched_files(
|
||||
&self,
|
||||
params: DidChangeWatchedFilesParams,
|
||||
) {
|
||||
// if the current import map has changed, we need to reload it
|
||||
let maybe_import_map_uri =
|
||||
self.maybe_import_map_uri.read().unwrap().clone();
|
||||
if let Some(import_map_uri) = maybe_import_map_uri {
|
||||
if params.changes.iter().any(|fe| import_map_uri == fe.uri) {
|
||||
if let Err(err) = self.update_import_map().await {
|
||||
self
|
||||
.client
|
||||
.show_message(MessageType::Warning, err.to_string())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn formatting(
|
||||
&self,
|
||||
params: DocumentFormattingParams,
|
||||
) -> LSPResult<Option<Vec<TextEdit>>> {
|
||||
let specifier = utils::normalize_url(params.text_document.uri.clone());
|
||||
let file_text = {
|
||||
let file_cache = self.file_cache.read().unwrap();
|
||||
let file_id = file_cache.lookup(&specifier).unwrap();
|
||||
// TODO(lucacasonato): handle error properly
|
||||
file_cache.get_contents(file_id).unwrap()
|
||||
};
|
||||
|
||||
let file_path =
|
||||
if let Ok(file_path) = params.text_document.uri.to_file_path() {
|
||||
file_path
|
||||
} else {
|
||||
PathBuf::from(params.text_document.uri.path())
|
||||
};
|
||||
|
||||
// TODO(lucacasonato): handle error properly
|
||||
let text_edits = tokio::task::spawn_blocking(move || {
|
||||
let config = dprint::configuration::ConfigurationBuilder::new()
|
||||
.deno()
|
||||
.build();
|
||||
// TODO(@kitsonk) this could be handled better in `cli/tools/fmt.rs` in the
|
||||
// future.
|
||||
match dprint::format_text(&file_path, &file_text, &config) {
|
||||
Ok(new_text) => Some(text::get_edits(&file_text, &new_text)),
|
||||
Err(err) => {
|
||||
warn!("Format error: {}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if let Some(text_edits) = text_edits {
|
||||
if text_edits.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(text_edits))
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn hover(&self, params: HoverParams) -> LSPResult<Option<Hover>> {
|
||||
let specifier = utils::normalize_url(
|
||||
params.text_document_position_params.text_document.uri,
|
||||
);
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let line_index = self.get_line_index(specifier.clone()).await.unwrap();
|
||||
let req = tsc::RequestMethod::GetQuickInfo((
|
||||
specifier,
|
||||
text::to_char_pos(
|
||||
&line_index,
|
||||
params.text_document_position_params.position,
|
||||
),
|
||||
));
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let res = self.ts_server.request(self.snapshot(), req).await.unwrap();
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let maybe_quick_info: Option<tsc::QuickInfo> =
|
||||
serde_json::from_value(res).unwrap();
|
||||
if let Some(quick_info) = maybe_quick_info {
|
||||
Ok(Some(quick_info.to_hover(&line_index)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn document_highlight(
|
||||
&self,
|
||||
params: DocumentHighlightParams,
|
||||
) -> LSPResult<Option<Vec<DocumentHighlight>>> {
|
||||
let specifier = utils::normalize_url(
|
||||
params.text_document_position_params.text_document.uri,
|
||||
);
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let line_index = self.get_line_index(specifier.clone()).await.unwrap();
|
||||
let files_to_search = vec![specifier.clone()];
|
||||
let req = tsc::RequestMethod::GetDocumentHighlights((
|
||||
specifier,
|
||||
text::to_char_pos(
|
||||
&line_index,
|
||||
params.text_document_position_params.position,
|
||||
),
|
||||
files_to_search,
|
||||
));
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let res = self.ts_server.request(self.snapshot(), req).await.unwrap();
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let maybe_document_highlights: Option<Vec<tsc::DocumentHighlights>> =
|
||||
serde_json::from_value(res).unwrap();
|
||||
|
||||
if let Some(document_highlights) = maybe_document_highlights {
|
||||
Ok(Some(
|
||||
document_highlights
|
||||
.into_iter()
|
||||
.map(|dh| dh.to_highlight(&line_index))
|
||||
.flatten()
|
||||
.collect(),
|
||||
))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn references(
|
||||
&self,
|
||||
params: ReferenceParams,
|
||||
) -> LSPResult<Option<Vec<Location>>> {
|
||||
let specifier =
|
||||
utils::normalize_url(params.text_document_position.text_document.uri);
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let line_index = self.get_line_index(specifier.clone()).await.unwrap();
|
||||
let req = tsc::RequestMethod::GetReferences((
|
||||
specifier,
|
||||
text::to_char_pos(&line_index, params.text_document_position.position),
|
||||
));
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let res = self.ts_server.request(self.snapshot(), req).await.unwrap();
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let maybe_references: Option<Vec<tsc::ReferenceEntry>> =
|
||||
serde_json::from_value(res).unwrap();
|
||||
|
||||
if let Some(references) = maybe_references {
|
||||
let mut results = Vec::new();
|
||||
for reference in references {
|
||||
if !params.context.include_declaration && reference.is_definition {
|
||||
continue;
|
||||
}
|
||||
let reference_specifier =
|
||||
ModuleSpecifier::resolve_url(&reference.file_name).unwrap();
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let line_index =
|
||||
self.get_line_index(reference_specifier).await.unwrap();
|
||||
results.push(reference.to_location(&line_index));
|
||||
}
|
||||
|
||||
Ok(Some(results))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn goto_definition(
|
||||
&self,
|
||||
params: GotoDefinitionParams,
|
||||
) -> LSPResult<Option<GotoDefinitionResponse>> {
|
||||
let specifier = utils::normalize_url(
|
||||
params.text_document_position_params.text_document.uri,
|
||||
);
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let line_index = self.get_line_index(specifier.clone()).await.unwrap();
|
||||
let req = tsc::RequestMethod::GetDefinition((
|
||||
specifier,
|
||||
text::to_char_pos(
|
||||
&line_index,
|
||||
params.text_document_position_params.position,
|
||||
),
|
||||
));
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let res = self.ts_server.request(self.snapshot(), req).await.unwrap();
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let maybe_definition: Option<tsc::DefinitionInfoAndBoundSpan> =
|
||||
serde_json::from_value(res).unwrap();
|
||||
|
||||
if let Some(definition) = maybe_definition {
|
||||
Ok(
|
||||
definition
|
||||
.to_definition(&line_index, |s| self.get_line_index(s))
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn completion(
|
||||
&self,
|
||||
params: CompletionParams,
|
||||
) -> LSPResult<Option<CompletionResponse>> {
|
||||
let specifier =
|
||||
utils::normalize_url(params.text_document_position.text_document.uri);
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let line_index = self.get_line_index(specifier.clone()).await.unwrap();
|
||||
let req = tsc::RequestMethod::GetCompletions((
|
||||
specifier,
|
||||
text::to_char_pos(&line_index, params.text_document_position.position),
|
||||
tsc::UserPreferences {
|
||||
// TODO(lucacasonato): enable this. see https://github.com/denoland/deno/pull/8651
|
||||
include_completions_with_insert_text: Some(false),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let res = self.ts_server.request(self.snapshot(), req).await.unwrap();
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let maybe_completion_info: Option<tsc::CompletionInfo> =
|
||||
serde_json::from_value(res).unwrap();
|
||||
|
||||
if let Some(completions) = maybe_completion_info {
|
||||
Ok(Some(completions.into_completion_response(&line_index)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn request_else(
|
||||
&self,
|
||||
method: &str,
|
||||
params: Option<Value>,
|
||||
) -> LSPResult<Option<Value>> {
|
||||
match method {
|
||||
"deno/virtualTextDocument" => match params.map(serde_json::from_value) {
|
||||
Some(Ok(params)) => Ok(Some(
|
||||
serde_json::to_value(self.virtual_text_document(params).await?)
|
||||
.map_err(|err| {
|
||||
error!(
|
||||
"Failed to serialize virtual_text_document response: {:#?}",
|
||||
err
|
||||
);
|
||||
LSPError::internal_error()
|
||||
})?,
|
||||
)),
|
||||
Some(Err(err)) => Err(LSPError::invalid_params(err.to_string())),
|
||||
None => Err(LSPError::invalid_params("Missing parameters")),
|
||||
},
|
||||
_ => {
|
||||
error!("Got a {} request, but no handler is defined", method);
|
||||
Err(LSPError::method_not_found())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageServer {
|
||||
async fn virtual_text_document(
|
||||
&self,
|
||||
params: VirtualTextDocumentParams,
|
||||
) -> LSPResult<Option<String>> {
|
||||
let specifier = utils::normalize_url(params.text_document.uri);
|
||||
let url = specifier.as_url();
|
||||
let contents = if url.as_str() == "deno:/status.md" {
|
||||
let file_cache = self.file_cache.read().unwrap();
|
||||
Some(format!(
|
||||
r#"# Deno Language Server Status
|
||||
|
||||
- Documents in memory: {}
|
||||
|
||||
"#,
|
||||
file_cache.len()
|
||||
))
|
||||
} else {
|
||||
match url.scheme() {
|
||||
"asset" => {
|
||||
let state_snapshot = self.snapshot();
|
||||
if let Some(text) =
|
||||
tsc::get_asset(&specifier, &self.ts_server, &state_snapshot)
|
||||
.await
|
||||
.map_err(|_| LSPError::new(LSPErrorCode::InternalError))?
|
||||
{
|
||||
Some(text)
|
||||
} else {
|
||||
error!("Missing asset: {}", specifier);
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let mut sources = self.sources.write().unwrap();
|
||||
if let Some(text) = sources.get_text(&specifier) {
|
||||
Some(text)
|
||||
} else {
|
||||
error!("The cached sources was not found: {}", specifier);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(contents)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DocumentData {
|
||||
pub dependencies: Option<HashMap<String, analysis::Dependency>>,
|
||||
pub version: Option<i32>,
|
||||
specifier: ModuleSpecifier,
|
||||
}
|
||||
|
||||
impl DocumentData {
|
||||
pub fn new(
|
||||
specifier: ModuleSpecifier,
|
||||
version: i32,
|
||||
source: &str,
|
||||
maybe_import_map: Option<ImportMap>,
|
||||
) -> Self {
|
||||
let dependencies = if let Some((dependencies, _)) =
|
||||
analysis::analyze_dependencies(
|
||||
&specifier,
|
||||
source,
|
||||
&MediaType::from(&specifier),
|
||||
&maybe_import_map,
|
||||
) {
|
||||
Some(dependencies)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Self {
|
||||
dependencies,
|
||||
version: Some(version),
|
||||
specifier,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
version: i32,
|
||||
source: &str,
|
||||
maybe_import_map: &Option<ImportMap>,
|
||||
) {
|
||||
self.dependencies = if let Some((dependencies, _)) =
|
||||
analysis::analyze_dependencies(
|
||||
&self.specifier,
|
||||
source,
|
||||
&MediaType::from(&self.specifier),
|
||||
maybe_import_map,
|
||||
) {
|
||||
Some(dependencies)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.version = Some(version)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VirtualTextDocumentParams {
|
||||
pub text_document: TextDocumentIdentifier,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use lspower::jsonrpc;
|
||||
use lspower::ExitedError;
|
||||
use lspower::LspService;
|
||||
use std::fs;
|
||||
use std::task::Poll;
|
||||
use tower_test::mock::Spawn;
|
||||
|
||||
enum LspResponse {
|
||||
None,
|
||||
RequestAny,
|
||||
Request(u64, Value),
|
||||
}
|
||||
|
||||
struct LspTestHarness {
|
||||
requests: Vec<(&'static str, LspResponse)>,
|
||||
service: Spawn<LspService>,
|
||||
}
|
||||
|
||||
impl LspTestHarness {
|
||||
pub fn new(requests: Vec<(&'static str, LspResponse)>) -> Self {
|
||||
let (service, _) = LspService::new(LanguageServer::new);
|
||||
let service = Spawn::new(service);
|
||||
Self { requests, service }
|
||||
}
|
||||
|
||||
async fn run(&mut self) {
|
||||
for (req_path_str, expected) in self.requests.iter() {
|
||||
assert_eq!(self.service.poll_ready(), Poll::Ready(Ok(())));
|
||||
let fixtures_path = test_util::root_path().join("cli/tests/lsp");
|
||||
assert!(fixtures_path.is_dir());
|
||||
let req_path = fixtures_path.join(req_path_str);
|
||||
let req_str = fs::read_to_string(req_path).unwrap();
|
||||
let req: jsonrpc::Incoming = serde_json::from_str(&req_str).unwrap();
|
||||
let response: Result<Option<jsonrpc::Outgoing>, ExitedError> =
|
||||
self.service.call(req).await;
|
||||
match response {
|
||||
Ok(result) => match expected {
|
||||
LspResponse::None => assert_eq!(result, None),
|
||||
LspResponse::RequestAny => match result {
|
||||
Some(jsonrpc::Outgoing::Response(_)) => (),
|
||||
_ => panic!("unexpected result: {:?}", result),
|
||||
},
|
||||
LspResponse::Request(id, value) => match result {
|
||||
Some(jsonrpc::Outgoing::Response(resp)) => assert_eq!(
|
||||
resp,
|
||||
jsonrpc::Response::ok(jsonrpc::Id::Number(*id), value.clone())
|
||||
),
|
||||
_ => panic!("unexpected result: {:?}", result),
|
||||
},
|
||||
},
|
||||
Err(err) => panic!("Error result: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_startup_shutdown() {
|
||||
let mut harness = LspTestHarness::new(vec![
|
||||
("initialize_request.json", LspResponse::RequestAny),
|
||||
("initialized_notification.json", LspResponse::None),
|
||||
(
|
||||
"shutdown_request.json",
|
||||
LspResponse::Request(3, json!(null)),
|
||||
),
|
||||
("exit_notification.json", LspResponse::None),
|
||||
]);
|
||||
harness.run().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_hover() {
|
||||
let mut harness = LspTestHarness::new(vec![
|
||||
("initialize_request.json", LspResponse::RequestAny),
|
||||
("initialized_notification.json", LspResponse::None),
|
||||
("did_open_notification.json", LspResponse::None),
|
||||
(
|
||||
"hover_request.json",
|
||||
LspResponse::Request(
|
||||
2,
|
||||
json!({
|
||||
"contents": [
|
||||
{
|
||||
"language": "typescript",
|
||||
"value": "const Deno.args: string[]"
|
||||
},
|
||||
"Returns the script arguments to the program. If for example we run a\nprogram:\n\ndeno run --allow-read https://deno.land/std/examples/cat.ts /etc/passwd\n\nThen `Deno.args` will contain:\n\n[ \"/etc/passwd\" ]"
|
||||
],
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 0,
|
||||
"character": 17
|
||||
},
|
||||
"end": {
|
||||
"line": 0,
|
||||
"character": 21
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
(
|
||||
"shutdown_request.json",
|
||||
LspResponse::Request(3, json!(null)),
|
||||
),
|
||||
("exit_notification.json", LspResponse::None),
|
||||
]);
|
||||
harness.run().await;
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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 {
|
||||
|
|
469
cli/lsp/mod.rs
469
cli/lsp/mod.rs
|
@ -1,472 +1,29 @@
|
|||
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||
use deno_core::error::AnyError;
|
||||
use lspower::LspService;
|
||||
use lspower::Server;
|
||||
|
||||
mod analysis;
|
||||
mod capabilities;
|
||||
mod config;
|
||||
mod diagnostics;
|
||||
mod dispatch;
|
||||
mod handlers;
|
||||
mod lsp_extensions;
|
||||
mod language_server;
|
||||
mod memory_cache;
|
||||
mod sources;
|
||||
mod state;
|
||||
mod text;
|
||||
mod tsc;
|
||||
mod utils;
|
||||
|
||||
use config::Config;
|
||||
use diagnostics::DiagnosticSource;
|
||||
use dispatch::NotificationDispatcher;
|
||||
use dispatch::RequestDispatcher;
|
||||
use state::update_import_map;
|
||||
use state::DocumentData;
|
||||
use state::Event;
|
||||
use state::ServerState;
|
||||
use state::Status;
|
||||
use state::Task;
|
||||
use text::apply_content_changes;
|
||||
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,
|
||||
¶ms.text_document.text,
|
||||
state.maybe_import_map.clone(),
|
||||
),
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
error!("duplicate DidOpenTextDocument: {}", specifier);
|
||||
}
|
||||
state
|
||||
.file_cache
|
||||
.write()
|
||||
.unwrap()
|
||||
.set_contents(specifier, Some(params.text_document.text.into_bytes()));
|
||||
|
||||
Ok(())
|
||||
})?
|
||||
.on::<lsp_types::notification::DidChangeTextDocument>(|state, params| {
|
||||
let specifier = utils::normalize_url(params.text_document.uri);
|
||||
let mut file_cache = state.file_cache.write().unwrap();
|
||||
let file_id = file_cache.lookup(&specifier).unwrap();
|
||||
let mut content = file_cache.get_contents(file_id)?;
|
||||
apply_content_changes(&mut content, params.content_changes);
|
||||
let doc_data = state.doc_data.get_mut(&specifier).unwrap();
|
||||
doc_data.update(
|
||||
params.text_document.version,
|
||||
&content,
|
||||
state.maybe_import_map.clone(),
|
||||
);
|
||||
file_cache.set_contents(specifier, Some(content.into_bytes()));
|
||||
|
||||
Ok(())
|
||||
})?
|
||||
.on::<lsp_types::notification::DidCloseTextDocument>(|state, params| {
|
||||
if params.text_document.uri.scheme() == "deno" {
|
||||
// we can ignore virtual text documents opening, as they don't need to
|
||||
// be tracked in memory, as they are static assets that won't change
|
||||
// already managed by the language service
|
||||
return Ok(());
|
||||
}
|
||||
let specifier = utils::normalize_url(params.text_document.uri);
|
||||
if state.doc_data.remove(&specifier).is_none() {
|
||||
error!("orphaned document: {}", specifier);
|
||||
}
|
||||
// TODO(@kitsonk) should we do garbage collection on the diagnostics?
|
||||
|
||||
Ok(())
|
||||
})?
|
||||
.on::<lsp_types::notification::DidSaveTextDocument>(|_state, _params| {
|
||||
// nothing to do yet... cleanup things?
|
||||
|
||||
Ok(())
|
||||
})?
|
||||
.on::<lsp_types::notification::DidChangeConfiguration>(|state, _params| {
|
||||
state.send_request::<lsp_types::request::WorkspaceConfiguration>(
|
||||
lsp_types::ConfigurationParams {
|
||||
items: vec![lsp_types::ConfigurationItem {
|
||||
scope_uri: None,
|
||||
section: Some("deno".to_string()),
|
||||
}],
|
||||
},
|
||||
|state, response| {
|
||||
let Response { error, result, .. } = response;
|
||||
|
||||
match (error, result) {
|
||||
(Some(err), _) => {
|
||||
error!("failed to fetch the extension settings: {:?}", err);
|
||||
}
|
||||
(None, Some(config)) => {
|
||||
if let Some(config) = config.get(0) {
|
||||
if let Err(err) = state.config.update(config.clone()) {
|
||||
error!("failed to update settings: {}", err);
|
||||
}
|
||||
if let Err(err) = update_import_map(state) {
|
||||
state
|
||||
.send_notification::<lsp_types::notification::ShowMessage>(
|
||||
lsp_types::ShowMessageParams {
|
||||
typ: lsp_types::MessageType::Warning,
|
||||
message: err.to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, None) => {
|
||||
error!("received empty extension settings from the client");
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
})?
|
||||
.on::<lsp_types::notification::DidChangeWatchedFiles>(|state, params| {
|
||||
// if the current import map has changed, we need to reload it
|
||||
if let Some(import_map_uri) = &state.maybe_import_map_uri {
|
||||
if params.changes.iter().any(|fe| import_map_uri == &fe.uri) {
|
||||
update_import_map(state)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?
|
||||
.finish();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_request(
|
||||
&mut self,
|
||||
request: Request,
|
||||
received: Instant,
|
||||
) -> Result<(), AnyError> {
|
||||
self.register_request(&request, received);
|
||||
|
||||
if self.shutdown_requested {
|
||||
self.respond(Response::new_err(
|
||||
request.id,
|
||||
ErrorCode::InvalidRequest as i32,
|
||||
"Shutdown already requested".to_string(),
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.status == Status::Loading && request.method != "shutdown" {
|
||||
self.respond(Response::new_err(
|
||||
request.id,
|
||||
ErrorCode::ContentModified as i32,
|
||||
"Deno Language Server is still loading...".to_string(),
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
RequestDispatcher {
|
||||
request: Some(request),
|
||||
server_state: self,
|
||||
}
|
||||
.on_sync::<lsp_types::request::Shutdown>(|s, ()| {
|
||||
s.shutdown_requested = true;
|
||||
Ok(())
|
||||
})?
|
||||
.on_sync::<lsp_types::request::DocumentHighlightRequest>(
|
||||
handlers::handle_document_highlight,
|
||||
)?
|
||||
.on_sync::<lsp_types::request::GotoDefinition>(
|
||||
handlers::handle_goto_definition,
|
||||
)?
|
||||
.on_sync::<lsp_types::request::HoverRequest>(handlers::handle_hover)?
|
||||
.on_sync::<lsp_types::request::Completion>(handlers::handle_completion)?
|
||||
.on_sync::<lsp_types::request::References>(handlers::handle_references)?
|
||||
.on_sync::<lsp_extensions::VirtualTextDocument>(
|
||||
handlers::handle_virtual_text_document,
|
||||
)?
|
||||
.on::<lsp_types::request::Formatting>(handlers::handle_formatting)
|
||||
.finish();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start consuming events from the provided receiver channel.
|
||||
pub fn run(mut self, inbox: Receiver<Message>) -> Result<(), AnyError> {
|
||||
// Check to see if we need to setup the import map
|
||||
if let Err(err) = update_import_map(&mut self) {
|
||||
self.send_notification::<lsp_types::notification::ShowMessage>(
|
||||
lsp_types::ShowMessageParams {
|
||||
typ: lsp_types::MessageType::Warning,
|
||||
message: err.to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// we are going to watch all the JSON files in the workspace, and the
|
||||
// notification handler will pick up any of the changes of those files we
|
||||
// are interested in.
|
||||
let watch_registration_options =
|
||||
lsp_types::DidChangeWatchedFilesRegistrationOptions {
|
||||
watchers: vec![lsp_types::FileSystemWatcher {
|
||||
glob_pattern: "**/*.json".to_string(),
|
||||
kind: Some(lsp_types::WatchKind::Change),
|
||||
}],
|
||||
};
|
||||
let registration = lsp_types::Registration {
|
||||
id: "workspace/didChangeWatchedFiles".to_string(),
|
||||
method: "workspace/didChangeWatchedFiles".to_string(),
|
||||
register_options: Some(
|
||||
serde_json::to_value(watch_registration_options).unwrap(),
|
||||
),
|
||||
};
|
||||
self.send_request::<lsp_types::request::RegisterCapability>(
|
||||
lsp_types::RegistrationParams {
|
||||
registrations: vec![registration],
|
||||
},
|
||||
|_, _| (),
|
||||
);
|
||||
|
||||
self.transition(Status::Ready);
|
||||
|
||||
while let Some(event) = self.next_event(&inbox) {
|
||||
if let Event::Message(Message::Notification(notification)) = &event {
|
||||
if notification.method == lsp_types::notification::Exit::METHOD {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
self.handle_event(event)?
|
||||
}
|
||||
|
||||
Err(custom_error(
|
||||
"ClientError",
|
||||
"Client exited without proper shutdown sequence.",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
395
cli/lsp/state.rs
395
cli/lsp/state.rs
|
@ -1,395 +0,0 @@
|
|||
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
use super::analysis;
|
||||
use super::config::Config;
|
||||
use super::diagnostics::DiagnosticCollection;
|
||||
use super::diagnostics::DiagnosticSource;
|
||||
use super::diagnostics::DiagnosticVec;
|
||||
use super::memory_cache::MemoryCache;
|
||||
use super::sources::Sources;
|
||||
use super::tsc;
|
||||
use super::utils::notification_is;
|
||||
|
||||
use crate::deno_dir;
|
||||
use crate::import_map::ImportMap;
|
||||
use crate::media_type::MediaType;
|
||||
|
||||
use crossbeam_channel::select;
|
||||
use crossbeam_channel::unbounded;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use deno_core::error::anyhow;
|
||||
use deno_core::error::AnyError;
|
||||
use deno_core::url::Url;
|
||||
use deno_core::JsRuntime;
|
||||
use deno_core::ModuleSpecifier;
|
||||
use lsp_server::Message;
|
||||
use lsp_server::Notification;
|
||||
use lsp_server::Request;
|
||||
use lsp_server::RequestId;
|
||||
use lsp_server::Response;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
use std::time::Instant;
|
||||
|
||||
type ReqHandler = fn(&mut ServerState, Response);
|
||||
type ReqQueue = lsp_server::ReqQueue<(String, Instant), ReqHandler>;
|
||||
|
||||
pub fn update_import_map(state: &mut ServerState) -> Result<(), AnyError> {
|
||||
if let Some(import_map_str) = &state.config.settings.import_map {
|
||||
let import_map_url = if let Ok(url) = Url::from_file_path(import_map_str) {
|
||||
Ok(url)
|
||||
} else if let Some(root_uri) = &state.config.root_uri {
|
||||
let root_path = root_uri
|
||||
.to_file_path()
|
||||
.map_err(|_| anyhow!("Bad root_uri: {}", root_uri))?;
|
||||
let import_map_path = root_path.join(import_map_str);
|
||||
Url::from_file_path(import_map_path).map_err(|_| {
|
||||
anyhow!("Bad file path for import map: {:?}", import_map_str)
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"The path to the import map (\"{}\") is not resolvable.",
|
||||
import_map_str
|
||||
))
|
||||
}?;
|
||||
let import_map_path = import_map_url
|
||||
.to_file_path()
|
||||
.map_err(|_| anyhow!("Bad file path."))?;
|
||||
let import_map_json =
|
||||
fs::read_to_string(import_map_path).map_err(|err| {
|
||||
anyhow!(
|
||||
"Failed to load the import map at: {}. [{}]",
|
||||
import_map_url,
|
||||
err
|
||||
)
|
||||
})?;
|
||||
let import_map =
|
||||
ImportMap::from_json(&import_map_url.to_string(), &import_map_json)?;
|
||||
state.maybe_import_map_uri = Some(import_map_url);
|
||||
state.maybe_import_map = Some(Arc::new(RwLock::new(import_map)));
|
||||
} else {
|
||||
state.maybe_import_map = None;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Message(Message),
|
||||
Task(Task),
|
||||
}
|
||||
|
||||
impl fmt::Debug for Event {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let debug_verbose_not =
|
||||
|notification: &Notification, f: &mut fmt::Formatter| {
|
||||
f.debug_struct("Notification")
|
||||
.field("method", ¬ification.method)
|
||||
.finish()
|
||||
};
|
||||
|
||||
match self {
|
||||
Event::Message(Message::Notification(notification)) => {
|
||||
if notification_is::<lsp_types::notification::DidOpenTextDocument>(
|
||||
notification,
|
||||
) || notification_is::<lsp_types::notification::DidChangeTextDocument>(
|
||||
notification,
|
||||
) {
|
||||
return debug_verbose_not(notification, f);
|
||||
}
|
||||
}
|
||||
Event::Task(Task::Response(response)) => {
|
||||
return f
|
||||
.debug_struct("Response")
|
||||
.field("id", &response.id)
|
||||
.field("error", &response.error)
|
||||
.finish();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
match self {
|
||||
Event::Message(it) => fmt::Debug::fmt(it, f),
|
||||
Event::Task(it) => fmt::Debug::fmt(it, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Copy, Clone)]
|
||||
pub enum Status {
|
||||
Loading,
|
||||
Ready,
|
||||
}
|
||||
|
||||
impl Default for Status {
|
||||
fn default() -> Self {
|
||||
Status::Loading
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Task {
|
||||
Diagnostics((DiagnosticSource, DiagnosticVec)),
|
||||
Response(Response),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DocumentData {
|
||||
pub dependencies: Option<HashMap<String, analysis::Dependency>>,
|
||||
pub version: Option<i32>,
|
||||
specifier: ModuleSpecifier,
|
||||
}
|
||||
|
||||
impl DocumentData {
|
||||
pub fn new(
|
||||
specifier: ModuleSpecifier,
|
||||
version: i32,
|
||||
source: &str,
|
||||
maybe_import_map: Option<Arc<RwLock<ImportMap>>>,
|
||||
) -> Self {
|
||||
let dependencies = if let Some((dependencies, _)) =
|
||||
analysis::analyze_dependencies(
|
||||
&specifier,
|
||||
source,
|
||||
&MediaType::from(&specifier),
|
||||
maybe_import_map,
|
||||
) {
|
||||
Some(dependencies)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Self {
|
||||
dependencies,
|
||||
version: Some(version),
|
||||
specifier,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
version: i32,
|
||||
source: &str,
|
||||
maybe_import_map: Option<Arc<RwLock<ImportMap>>>,
|
||||
) {
|
||||
self.dependencies = if let Some((dependencies, _)) =
|
||||
analysis::analyze_dependencies(
|
||||
&self.specifier,
|
||||
source,
|
||||
&MediaType::from(&self.specifier),
|
||||
maybe_import_map,
|
||||
) {
|
||||
Some(dependencies)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.version = Some(version)
|
||||
}
|
||||
}
|
||||
|
||||
/// An immutable snapshot of the server state at a point in time.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ServerStateSnapshot {
|
||||
pub assets: Arc<RwLock<HashMap<ModuleSpecifier, Option<String>>>>,
|
||||
pub config: Config,
|
||||
pub diagnostics: DiagnosticCollection,
|
||||
pub doc_data: HashMap<ModuleSpecifier, DocumentData>,
|
||||
pub file_cache: Arc<RwLock<MemoryCache>>,
|
||||
pub sources: Arc<RwLock<Sources>>,
|
||||
}
|
||||
|
||||
pub struct ServerState {
|
||||
pub assets: Arc<RwLock<HashMap<ModuleSpecifier, Option<String>>>>,
|
||||
pub config: Config,
|
||||
pub diagnostics: DiagnosticCollection,
|
||||
pub doc_data: HashMap<ModuleSpecifier, DocumentData>,
|
||||
pub file_cache: Arc<RwLock<MemoryCache>>,
|
||||
pub maybe_import_map: Option<Arc<RwLock<ImportMap>>>,
|
||||
pub maybe_import_map_uri: Option<Url>,
|
||||
req_queue: ReqQueue,
|
||||
sender: Sender<Message>,
|
||||
pub sources: Arc<RwLock<Sources>>,
|
||||
pub shutdown_requested: bool,
|
||||
pub status: Status,
|
||||
task_sender: Sender<Task>,
|
||||
pub task_receiver: Receiver<Task>,
|
||||
pub ts_runtime: JsRuntime,
|
||||
}
|
||||
|
||||
impl ServerState {
|
||||
pub fn new(sender: Sender<Message>, config: Config) -> Self {
|
||||
let (task_sender, task_receiver) = unbounded();
|
||||
let custom_root = env::var("DENO_DIR").map(String::into).ok();
|
||||
let dir =
|
||||
deno_dir::DenoDir::new(custom_root).expect("could not access DENO_DIR");
|
||||
let location = dir.root.join("deps");
|
||||
let sources = Sources::new(&location);
|
||||
// TODO(@kitsonk) we need to allow displaying diagnostics here, but the
|
||||
// current compiler snapshot sends them to stdio which would totally break
|
||||
// the language server...
|
||||
let ts_runtime = tsc::start(false).expect("could not start tsc");
|
||||
|
||||
Self {
|
||||
assets: Default::default(),
|
||||
config,
|
||||
diagnostics: Default::default(),
|
||||
doc_data: Default::default(),
|
||||
file_cache: Arc::new(RwLock::new(Default::default())),
|
||||
maybe_import_map: None,
|
||||
maybe_import_map_uri: None,
|
||||
req_queue: Default::default(),
|
||||
sender,
|
||||
sources: Arc::new(RwLock::new(sources)),
|
||||
shutdown_requested: false,
|
||||
status: Default::default(),
|
||||
task_receiver,
|
||||
task_sender,
|
||||
ts_runtime,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, request_id: RequestId) {
|
||||
if let Some(response) = self.req_queue.incoming.cancel(request_id) {
|
||||
self.send(response.into());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn complete_request(&mut self, response: Response) {
|
||||
let handler = self.req_queue.outgoing.complete(response.id.clone());
|
||||
handler(self, response)
|
||||
}
|
||||
|
||||
pub fn next_event(&self, inbox: &Receiver<Message>) -> Option<Event> {
|
||||
select! {
|
||||
recv(inbox) -> msg => msg.ok().map(Event::Message),
|
||||
recv(self.task_receiver) -> task => Some(Event::Task(task.unwrap())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle any changes and return a `bool` that indicates if there were
|
||||
/// important changes to the state.
|
||||
pub fn process_changes(&mut self) -> bool {
|
||||
let mut file_cache = self.file_cache.write().unwrap();
|
||||
let changed_files = file_cache.take_changes();
|
||||
// other processing of changed files should be done here as needed
|
||||
!changed_files.is_empty()
|
||||
}
|
||||
|
||||
pub fn register_request(&mut self, request: &Request, received: Instant) {
|
||||
self
|
||||
.req_queue
|
||||
.incoming
|
||||
.register(request.id.clone(), (request.method.clone(), received));
|
||||
}
|
||||
|
||||
pub fn respond(&mut self, response: Response) {
|
||||
if let Some((_, _)) = self.req_queue.incoming.complete(response.id.clone())
|
||||
{
|
||||
self.send(response.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn send(&mut self, message: Message) {
|
||||
self.sender.send(message).unwrap()
|
||||
}
|
||||
|
||||
pub fn send_notification<N: lsp_types::notification::Notification>(
|
||||
&mut self,
|
||||
params: N::Params,
|
||||
) {
|
||||
let notification = Notification::new(N::METHOD.to_string(), params);
|
||||
self.send(notification.into());
|
||||
}
|
||||
|
||||
pub fn send_request<R: lsp_types::request::Request>(
|
||||
&mut self,
|
||||
params: R::Params,
|
||||
handler: ReqHandler,
|
||||
) {
|
||||
let request =
|
||||
self
|
||||
.req_queue
|
||||
.outgoing
|
||||
.register(R::METHOD.to_string(), params, handler);
|
||||
self.send(request.into());
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> ServerStateSnapshot {
|
||||
ServerStateSnapshot {
|
||||
assets: Arc::clone(&self.assets),
|
||||
config: self.config.clone(),
|
||||
diagnostics: self.diagnostics.clone(),
|
||||
doc_data: self.doc_data.clone(),
|
||||
file_cache: Arc::clone(&self.file_cache),
|
||||
sources: Arc::clone(&self.sources),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn<F>(&mut self, task: F)
|
||||
where
|
||||
F: FnOnce() -> Task + Send + 'static,
|
||||
{
|
||||
let sender = self.task_sender.clone();
|
||||
tokio::task::spawn_blocking(move || sender.send(task()).unwrap());
|
||||
}
|
||||
|
||||
pub fn transition(&mut self, new_status: Status) {
|
||||
self.status = new_status;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use deno_core::serde_json::json;
|
||||
use deno_core::serde_json::Value;
|
||||
use lsp_server::Connection;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_update_import_map() {
|
||||
let temp_dir = TempDir::new().expect("could not create temp dir");
|
||||
let import_map_path = temp_dir.path().join("import_map.json");
|
||||
let import_map_str = &import_map_path.to_string_lossy();
|
||||
fs::write(
|
||||
import_map_path.clone(),
|
||||
r#"{
|
||||
"imports": {
|
||||
"denoland/": "https://deno.land/x/"
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("could not write file");
|
||||
let mut config = Config::default();
|
||||
config
|
||||
.update(json!({
|
||||
"enable": false,
|
||||
"config": Value::Null,
|
||||
"lint": false,
|
||||
"importMap": import_map_str,
|
||||
"unstable": true,
|
||||
}))
|
||||
.expect("could not update config");
|
||||
let (connection, _) = Connection::memory();
|
||||
let mut state = ServerState::new(connection.sender, config);
|
||||
let result = update_import_map(&mut state);
|
||||
assert!(result.is_ok());
|
||||
assert!(state.maybe_import_map.is_some());
|
||||
let expected =
|
||||
Url::from_file_path(import_map_path).expect("could not parse url");
|
||||
assert_eq!(state.maybe_import_map_uri, Some(expected));
|
||||
let import_map = state.maybe_import_map.unwrap();
|
||||
let import_map = import_map.read().unwrap();
|
||||
assert_eq!(
|
||||
import_map
|
||||
.resolve("denoland/mod.ts", "https://example.com/index.js")
|
||||
.expect("bad response"),
|
||||
Some(
|
||||
ModuleSpecifier::resolve_url("https://deno.land/x/mod.ts")
|
||||
.expect("could not create URL")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
227
cli/lsp/tsc.rs
227
cli/lsp/tsc.rs
|
@ -1,18 +1,21 @@
|
|||
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
use super::analysis::ResolvedImport;
|
||||
use super::state::ServerStateSnapshot;
|
||||
use super::language_server::StateSnapshot;
|
||||
use super::text;
|
||||
use super::utils;
|
||||
|
||||
use crate::js;
|
||||
use crate::media_type::MediaType;
|
||||
use crate::tokio_util::create_basic_runtime;
|
||||
use crate::tsc;
|
||||
use crate::tsc::ResolveArgs;
|
||||
use crate::tsc_config::TsConfig;
|
||||
|
||||
use deno_core::error::anyhow;
|
||||
use deno_core::error::custom_error;
|
||||
use deno_core::error::AnyError;
|
||||
use deno_core::futures::Future;
|
||||
use deno_core::json_op_sync;
|
||||
use deno_core::serde::Deserialize;
|
||||
use deno_core::serde::Serialize;
|
||||
|
@ -23,31 +26,89 @@ use deno_core::JsRuntime;
|
|||
use deno_core::ModuleSpecifier;
|
||||
use deno_core::OpFn;
|
||||
use deno_core::RuntimeOptions;
|
||||
use lspower::lsp_types;
|
||||
use regex::Captures;
|
||||
use regex::Regex;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::thread;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
type Request = (
|
||||
RequestMethod,
|
||||
StateSnapshot,
|
||||
oneshot::Sender<Result<Value, AnyError>>,
|
||||
);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TsServer(mpsc::UnboundedSender<Request>);
|
||||
|
||||
impl TsServer {
|
||||
pub fn new() -> Self {
|
||||
let (tx, mut rx) = mpsc::unbounded_channel::<Request>();
|
||||
let _join_handle = thread::spawn(move || {
|
||||
// TODO(@kitsonk) we need to allow displaying diagnostics here, but the
|
||||
// current compiler snapshot sends them to stdio which would totally break
|
||||
// the language server...
|
||||
let mut ts_runtime = start(false).expect("could not start tsc");
|
||||
|
||||
let mut runtime = create_basic_runtime();
|
||||
runtime.block_on(async {
|
||||
while let Some((req, state_snapshot, tx)) = rx.recv().await {
|
||||
let value = request(&mut ts_runtime, state_snapshot, req);
|
||||
if tx.send(value).is_err() {
|
||||
warn!("Unable to send result to client.");
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
Self(tx)
|
||||
}
|
||||
|
||||
pub async fn request(
|
||||
&self,
|
||||
snapshot: StateSnapshot,
|
||||
req: RequestMethod,
|
||||
) -> Result<Value, AnyError> {
|
||||
let (tx, rx) = oneshot::channel::<Result<Value, AnyError>>();
|
||||
if self.0.send((req, snapshot, tx)).is_err() {
|
||||
return Err(anyhow!("failed to send request to tsc thread"));
|
||||
}
|
||||
rx.await?
|
||||
}
|
||||
}
|
||||
|
||||
/// Optionally returns an internal asset, first checking for any static assets
|
||||
/// in Rust, then checking any previously retrieved static assets from the
|
||||
/// isolate, and then finally, the tsc isolate itself.
|
||||
pub fn get_asset(
|
||||
pub async fn get_asset(
|
||||
specifier: &ModuleSpecifier,
|
||||
runtime: &mut JsRuntime,
|
||||
server_state: &ServerStateSnapshot,
|
||||
ts_server: &TsServer,
|
||||
state_snapshot: &StateSnapshot,
|
||||
) -> Result<Option<String>, AnyError> {
|
||||
let specifier_str = specifier.to_string().replace("asset:///", "");
|
||||
if let Some(asset_text) = tsc::get_asset(&specifier_str) {
|
||||
Ok(Some(asset_text.to_string()))
|
||||
} else {
|
||||
let mut assets = server_state.assets.write().unwrap();
|
||||
if let Some(asset) = assets.get(specifier) {
|
||||
Ok(asset.clone())
|
||||
} else {
|
||||
let asset = request_asset(specifier, runtime, server_state)?;
|
||||
assets.insert(specifier.clone(), asset.clone());
|
||||
Ok(asset)
|
||||
{
|
||||
let assets = state_snapshot.assets.read().unwrap();
|
||||
if let Some(asset) = assets.get(specifier) {
|
||||
return Ok(asset.clone());
|
||||
}
|
||||
}
|
||||
let asset: Option<String> = serde_json::from_value(
|
||||
ts_server
|
||||
.request(
|
||||
state_snapshot.clone(),
|
||||
RequestMethod::GetAsset(specifier.clone()),
|
||||
)
|
||||
.await?,
|
||||
)?;
|
||||
let mut assets = state_snapshot.assets.write().unwrap();
|
||||
assets.insert(specifier.clone(), asset.clone());
|
||||
Ok(asset)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -235,7 +296,7 @@ pub enum ScriptElementKind {
|
|||
|
||||
impl From<ScriptElementKind> for lsp_types::CompletionItemKind {
|
||||
fn from(kind: ScriptElementKind) -> Self {
|
||||
use lsp_types::CompletionItemKind;
|
||||
use lspower::lsp_types::CompletionItemKind;
|
||||
|
||||
match kind {
|
||||
ScriptElementKind::PrimitiveType | ScriptElementKind::Keyword => {
|
||||
|
@ -395,21 +456,21 @@ pub struct DefinitionInfoAndBoundSpan {
|
|||
}
|
||||
|
||||
impl DefinitionInfoAndBoundSpan {
|
||||
pub fn to_definition<F>(
|
||||
pub async fn to_definition<F, Fut>(
|
||||
&self,
|
||||
line_index: &[u32],
|
||||
mut index_provider: F,
|
||||
index_provider: F,
|
||||
) -> Option<lsp_types::GotoDefinitionResponse>
|
||||
where
|
||||
F: FnMut(ModuleSpecifier) -> Vec<u32>,
|
||||
F: Fn(ModuleSpecifier) -> Fut,
|
||||
Fut: Future<Output = Result<Vec<u32>, AnyError>>,
|
||||
{
|
||||
if let Some(definitions) = &self.definitions {
|
||||
let location_links = definitions
|
||||
.iter()
|
||||
.map(|di| {
|
||||
let target_specifier =
|
||||
ModuleSpecifier::resolve_url(&di.file_name).unwrap();
|
||||
let target_line_index = index_provider(target_specifier);
|
||||
let mut location_links = Vec::<lsp_types::LocationLink>::new();
|
||||
for di in definitions {
|
||||
let target_specifier =
|
||||
ModuleSpecifier::resolve_url(&di.file_name).unwrap();
|
||||
if let Ok(target_line_index) = index_provider(target_specifier).await {
|
||||
let target_uri = utils::normalize_file_name(&di.file_name).unwrap();
|
||||
let (target_range, target_selection_range) =
|
||||
if let Some(context_span) = &di.context_span {
|
||||
|
@ -423,15 +484,14 @@ impl DefinitionInfoAndBoundSpan {
|
|||
di.text_span.to_range(&target_line_index),
|
||||
)
|
||||
};
|
||||
lsp_types::LocationLink {
|
||||
location_links.push(lsp_types::LocationLink {
|
||||
origin_selection_range: Some(self.text_span.to_range(line_index)),
|
||||
target_uri,
|
||||
target_range,
|
||||
target_selection_range,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
Some(lsp_types::GotoDefinitionResponse::Link(location_links))
|
||||
} else {
|
||||
None
|
||||
|
@ -599,17 +659,17 @@ struct State<'a> {
|
|||
asset: Option<String>,
|
||||
last_id: usize,
|
||||
response: Option<Response>,
|
||||
server_state: ServerStateSnapshot,
|
||||
state_snapshot: StateSnapshot,
|
||||
snapshots: HashMap<(Cow<'a, str>, Cow<'a, str>), String>,
|
||||
}
|
||||
|
||||
impl<'a> State<'a> {
|
||||
fn new(server_state: ServerStateSnapshot) -> Self {
|
||||
fn new(state_snapshot: StateSnapshot) -> Self {
|
||||
Self {
|
||||
asset: None,
|
||||
last_id: 1,
|
||||
response: None,
|
||||
server_state,
|
||||
state_snapshot,
|
||||
snapshots: Default::default(),
|
||||
}
|
||||
}
|
||||
|
@ -626,9 +686,11 @@ fn cache_snapshot(
|
|||
.contains_key(&(specifier.clone().into(), version.clone().into()))
|
||||
{
|
||||
let s = ModuleSpecifier::resolve_url(&specifier)?;
|
||||
let file_cache = state.server_state.file_cache.read().unwrap();
|
||||
let file_id = file_cache.lookup(&s).unwrap();
|
||||
let content = file_cache.get_contents(file_id)?;
|
||||
let content = {
|
||||
let file_cache = state.state_snapshot.file_cache.read().unwrap();
|
||||
let file_id = file_cache.lookup(&s).unwrap();
|
||||
file_cache.get_contents(file_id)?
|
||||
};
|
||||
state
|
||||
.snapshots
|
||||
.insert((specifier.into(), version.into()), content);
|
||||
|
@ -713,7 +775,7 @@ fn get_change_range(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
|||
fn get_length(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
||||
let v: SourceSnapshotArgs = serde_json::from_value(args)?;
|
||||
let specifier = ModuleSpecifier::resolve_url(&v.specifier)?;
|
||||
if state.server_state.doc_data.contains_key(&specifier) {
|
||||
if state.state_snapshot.doc_data.contains_key(&specifier) {
|
||||
cache_snapshot(state, v.specifier.clone(), v.version.clone())?;
|
||||
let content = state
|
||||
.snapshots
|
||||
|
@ -721,7 +783,7 @@ fn get_length(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
|||
.unwrap();
|
||||
Ok(json!(content.chars().count()))
|
||||
} else {
|
||||
let mut sources = state.server_state.sources.write().unwrap();
|
||||
let mut sources = state.state_snapshot.sources.write().unwrap();
|
||||
Ok(json!(sources.get_length(&specifier).unwrap()))
|
||||
}
|
||||
}
|
||||
|
@ -738,7 +800,7 @@ struct GetTextArgs {
|
|||
fn get_text(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
||||
let v: GetTextArgs = serde_json::from_value(args)?;
|
||||
let specifier = ModuleSpecifier::resolve_url(&v.specifier)?;
|
||||
let content = if state.server_state.doc_data.contains_key(&specifier) {
|
||||
let content = if state.state_snapshot.doc_data.contains_key(&specifier) {
|
||||
cache_snapshot(state, v.specifier.clone(), v.version.clone())?;
|
||||
state
|
||||
.snapshots
|
||||
|
@ -746,7 +808,7 @@ fn get_text(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
|||
.unwrap()
|
||||
.clone()
|
||||
} else {
|
||||
let mut sources = state.server_state.sources.write().unwrap();
|
||||
let mut sources = state.state_snapshot.sources.write().unwrap();
|
||||
sources.get_text(&specifier).unwrap()
|
||||
};
|
||||
Ok(json!(text::slice(&content, v.start..v.end)))
|
||||
|
@ -756,13 +818,13 @@ fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
|||
let v: ResolveArgs = serde_json::from_value(args)?;
|
||||
let mut resolved = Vec::<Option<(String, String)>>::new();
|
||||
let referrer = ModuleSpecifier::resolve_url(&v.base)?;
|
||||
let mut sources = if let Ok(sources) = state.server_state.sources.write() {
|
||||
let mut sources = if let Ok(sources) = state.state_snapshot.sources.write() {
|
||||
sources
|
||||
} else {
|
||||
return Err(custom_error("Deadlock", "deadlock locking sources"));
|
||||
};
|
||||
|
||||
if let Some(doc_data) = state.server_state.doc_data.get(&referrer) {
|
||||
if let Some(doc_data) = state.state_snapshot.doc_data.get(&referrer) {
|
||||
if let Some(dependencies) = &doc_data.dependencies {
|
||||
for specifier in &v.specifiers {
|
||||
if specifier.starts_with("asset:///") {
|
||||
|
@ -782,7 +844,7 @@ fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
|||
if let ResolvedImport::Resolved(resolved_specifier) = resolved_import
|
||||
{
|
||||
if state
|
||||
.server_state
|
||||
.state_snapshot
|
||||
.doc_data
|
||||
.contains_key(&resolved_specifier)
|
||||
|| sources.contains(&resolved_specifier)
|
||||
|
@ -837,7 +899,7 @@ fn respond(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
|||
|
||||
fn script_names(state: &mut State, _args: Value) -> Result<Value, AnyError> {
|
||||
let script_names: Vec<&ModuleSpecifier> =
|
||||
state.server_state.doc_data.keys().collect();
|
||||
state.state_snapshot.doc_data.keys().collect();
|
||||
Ok(json!(script_names))
|
||||
}
|
||||
|
||||
|
@ -850,13 +912,13 @@ struct ScriptVersionArgs {
|
|||
fn script_version(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
||||
let v: ScriptVersionArgs = serde_json::from_value(args)?;
|
||||
let specifier = ModuleSpecifier::resolve_url(&v.specifier)?;
|
||||
let maybe_doc_data = state.server_state.doc_data.get(&specifier);
|
||||
let maybe_doc_data = state.state_snapshot.doc_data.get(&specifier);
|
||||
if let Some(doc_data) = maybe_doc_data {
|
||||
if let Some(version) = doc_data.version {
|
||||
return Ok(json!(version.to_string()));
|
||||
}
|
||||
} else {
|
||||
let mut sources = state.server_state.sources.write().unwrap();
|
||||
let mut sources = state.state_snapshot.sources.write().unwrap();
|
||||
if let Some(version) = sources.get_script_version(&specifier) {
|
||||
return Ok(json!(version));
|
||||
}
|
||||
|
@ -889,7 +951,7 @@ pub fn start(debug: bool) -> Result<JsRuntime, AnyError> {
|
|||
{
|
||||
let op_state = runtime.op_state();
|
||||
let mut op_state = op_state.borrow_mut();
|
||||
op_state.put(State::new(ServerStateSnapshot::default()));
|
||||
op_state.put(State::new(StateSnapshot::default()));
|
||||
}
|
||||
|
||||
runtime.register_op("op_dispose", op(dispose));
|
||||
|
@ -1071,14 +1133,14 @@ impl RequestMethod {
|
|||
/// Send a request into a runtime and return the JSON value of the response.
|
||||
pub fn request(
|
||||
runtime: &mut JsRuntime,
|
||||
server_state: &ServerStateSnapshot,
|
||||
state_snapshot: StateSnapshot,
|
||||
method: RequestMethod,
|
||||
) -> Result<Value, AnyError> {
|
||||
let id = {
|
||||
let op_state = runtime.op_state();
|
||||
let mut op_state = op_state.borrow_mut();
|
||||
let state = op_state.borrow_mut::<State>();
|
||||
state.server_state = server_state.clone();
|
||||
state.state_snapshot = state_snapshot;
|
||||
state.last_id += 1;
|
||||
state.last_id
|
||||
};
|
||||
|
@ -1101,40 +1163,16 @@ pub fn request(
|
|||
}
|
||||
}
|
||||
|
||||
fn request_asset(
|
||||
specifier: &ModuleSpecifier,
|
||||
runtime: &mut JsRuntime,
|
||||
server_state: &ServerStateSnapshot,
|
||||
) -> Result<Option<String>, AnyError> {
|
||||
let id = {
|
||||
let op_state = runtime.op_state();
|
||||
let mut op_state = op_state.borrow_mut();
|
||||
let state = op_state.borrow_mut::<State>();
|
||||
state.server_state = server_state.clone();
|
||||
state.last_id += 1;
|
||||
state.last_id
|
||||
};
|
||||
let request_params = RequestMethod::GetAsset(specifier.clone()).to_value(id);
|
||||
let request_src = format!("globalThis.serverRequest({});", request_params);
|
||||
runtime.execute("[native_code]", &request_src)?;
|
||||
|
||||
let op_state = runtime.op_state();
|
||||
let mut op_state = op_state.borrow_mut();
|
||||
let state = op_state.borrow_mut::<State>();
|
||||
|
||||
Ok(state.asset.clone())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::memory_cache::MemoryCache;
|
||||
use super::super::state::DocumentData;
|
||||
use super::*;
|
||||
use crate::lsp::language_server::DocumentData;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
fn mock_server_state(sources: Vec<(&str, &str, i32)>) -> ServerStateSnapshot {
|
||||
fn mock_state_snapshot(sources: Vec<(&str, &str, i32)>) -> StateSnapshot {
|
||||
let mut doc_data = HashMap::new();
|
||||
let mut file_cache = MemoryCache::default();
|
||||
for (specifier, content, version) in sources {
|
||||
|
@ -1147,10 +1185,8 @@ mod tests {
|
|||
file_cache.set_contents(specifier, Some(content.as_bytes().to_vec()));
|
||||
}
|
||||
let file_cache = Arc::new(RwLock::new(file_cache));
|
||||
ServerStateSnapshot {
|
||||
StateSnapshot {
|
||||
assets: Default::default(),
|
||||
config: Default::default(),
|
||||
diagnostics: Default::default(),
|
||||
doc_data,
|
||||
file_cache,
|
||||
sources: Default::default(),
|
||||
|
@ -1161,20 +1197,20 @@ mod tests {
|
|||
debug: bool,
|
||||
config: Value,
|
||||
sources: Vec<(&str, &str, i32)>,
|
||||
) -> (JsRuntime, ServerStateSnapshot) {
|
||||
let server_state = mock_server_state(sources.clone());
|
||||
) -> (JsRuntime, StateSnapshot) {
|
||||
let state_snapshot = mock_state_snapshot(sources.clone());
|
||||
let mut runtime = start(debug).expect("could not start server");
|
||||
let ts_config = TsConfig::new(config);
|
||||
assert_eq!(
|
||||
request(
|
||||
&mut runtime,
|
||||
&server_state,
|
||||
state_snapshot.clone(),
|
||||
RequestMethod::Configure(ts_config)
|
||||
)
|
||||
.expect("failed request"),
|
||||
json!(true)
|
||||
);
|
||||
(runtime, server_state)
|
||||
(runtime, state_snapshot)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1207,7 +1243,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_project_reconfigure() {
|
||||
let (mut runtime, server_state) = setup(
|
||||
let (mut runtime, state_snapshot) = setup(
|
||||
false,
|
||||
json!({
|
||||
"target": "esnext",
|
||||
|
@ -1224,7 +1260,7 @@ mod tests {
|
|||
}));
|
||||
let result = request(
|
||||
&mut runtime,
|
||||
&server_state,
|
||||
state_snapshot,
|
||||
RequestMethod::Configure(ts_config),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
|
@ -1234,7 +1270,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_get_semantic_diagnostics() {
|
||||
let (mut runtime, server_state) = setup(
|
||||
let (mut runtime, state_snapshot) = setup(
|
||||
false,
|
||||
json!({
|
||||
"target": "esnext",
|
||||
|
@ -1247,7 +1283,7 @@ mod tests {
|
|||
.expect("could not resolve url");
|
||||
let result = request(
|
||||
&mut runtime,
|
||||
&server_state,
|
||||
state_snapshot,
|
||||
RequestMethod::GetSemanticDiagnostics(specifier),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
|
@ -1276,7 +1312,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_module_resolution() {
|
||||
let (mut runtime, server_state) = setup(
|
||||
let (mut runtime, state_snapshot) = setup(
|
||||
false,
|
||||
json!({
|
||||
"target": "esnext",
|
||||
|
@ -1300,7 +1336,7 @@ mod tests {
|
|||
.expect("could not resolve url");
|
||||
let result = request(
|
||||
&mut runtime,
|
||||
&server_state,
|
||||
state_snapshot,
|
||||
RequestMethod::GetSemanticDiagnostics(specifier),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
|
@ -1310,7 +1346,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_bad_module_specifiers() {
|
||||
let (mut runtime, server_state) = setup(
|
||||
let (mut runtime, state_snapshot) = setup(
|
||||
false,
|
||||
json!({
|
||||
"target": "esnext",
|
||||
|
@ -1330,7 +1366,7 @@ mod tests {
|
|||
.expect("could not resolve url");
|
||||
let result = request(
|
||||
&mut runtime,
|
||||
&server_state,
|
||||
state_snapshot,
|
||||
RequestMethod::GetSyntacticDiagnostics(specifier),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
|
@ -1340,7 +1376,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_remote_modules() {
|
||||
let (mut runtime, server_state) = setup(
|
||||
let (mut runtime, state_snapshot) = setup(
|
||||
false,
|
||||
json!({
|
||||
"target": "esnext",
|
||||
|
@ -1364,7 +1400,7 @@ mod tests {
|
|||
.expect("could not resolve url");
|
||||
let result = request(
|
||||
&mut runtime,
|
||||
&server_state,
|
||||
state_snapshot,
|
||||
RequestMethod::GetSyntacticDiagnostics(specifier),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
|
@ -1374,7 +1410,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_partial_modules() {
|
||||
let (mut runtime, server_state) = setup(
|
||||
let (mut runtime, state_snapshot) = setup(
|
||||
false,
|
||||
json!({
|
||||
"target": "esnext",
|
||||
|
@ -1401,7 +1437,7 @@ mod tests {
|
|||
.expect("could not resolve url");
|
||||
let result = request(
|
||||
&mut runtime,
|
||||
&server_state,
|
||||
state_snapshot,
|
||||
RequestMethod::GetSyntacticDiagnostics(specifier),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
|
@ -1428,7 +1464,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_request_asset() {
|
||||
let (mut runtime, server_state) = setup(
|
||||
let (mut runtime, state_snapshot) = setup(
|
||||
false,
|
||||
json!({
|
||||
"target": "esnext",
|
||||
|
@ -1440,9 +1476,14 @@ mod tests {
|
|||
);
|
||||
let specifier = ModuleSpecifier::resolve_url("asset:///lib.esnext.d.ts")
|
||||
.expect("could not resolve url");
|
||||
let result = request_asset(&specifier, &mut runtime, &server_state);
|
||||
let result = request(
|
||||
&mut runtime,
|
||||
state_snapshot,
|
||||
RequestMethod::GetAsset(specifier),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
let response: Option<String> =
|
||||
serde_json::from_value(result.unwrap()).unwrap();
|
||||
assert!(response.is_some());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -416,7 +416,7 @@ async fn install_command(
|
|||
}
|
||||
|
||||
async fn language_server_command() -> Result<(), AnyError> {
|
||||
lsp::start()
|
||||
lsp::start().await
|
||||
}
|
||||
|
||||
async fn lint_command(
|
||||
|
|
|
@ -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"));
|
||||
}
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue