mirror of
https://github.com/denoland/deno.git
synced 2024-10-30 09:08:00 -04:00
5005 lines
145 KiB
Rust
5005 lines
145 KiB
Rust
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
|
|
|
use deno_core::error::anyhow;
|
|
use deno_core::error::AnyError;
|
|
use deno_core::error::Context;
|
|
use deno_core::resolve_url;
|
|
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 log::error;
|
|
use log::info;
|
|
use log::warn;
|
|
use lspower::jsonrpc::Error as LspError;
|
|
use lspower::jsonrpc::Result as LspResult;
|
|
use lspower::lsp::request::*;
|
|
use lspower::lsp::*;
|
|
use lspower::Client;
|
|
use regex::Regex;
|
|
use serde_json::from_value;
|
|
use std::cell::RefCell;
|
|
use std::collections::HashMap;
|
|
use std::env;
|
|
use std::path::PathBuf;
|
|
use std::rc::Rc;
|
|
use std::sync::atomic::Ordering;
|
|
use std::sync::Arc;
|
|
use tokio::fs;
|
|
|
|
use crate::config_file::ConfigFile;
|
|
use crate::config_file::TsConfig;
|
|
use crate::deno_dir;
|
|
use crate::import_map::ImportMap;
|
|
use crate::logger;
|
|
use crate::media_type::MediaType;
|
|
|
|
use super::analysis;
|
|
use super::analysis::ts_changes_to_edit;
|
|
use super::analysis::CodeActionCollection;
|
|
use super::analysis::CodeActionData;
|
|
use super::analysis::CodeLensData;
|
|
use super::analysis::CodeLensSource;
|
|
use super::analysis::ResolvedDependency;
|
|
use super::capabilities;
|
|
use super::completions;
|
|
use super::config::Config;
|
|
use super::config::SETTINGS_SECTION;
|
|
use super::diagnostics;
|
|
use super::diagnostics::DiagnosticSource;
|
|
use super::documents::DocumentCache;
|
|
use super::performance::Performance;
|
|
use super::registries;
|
|
use super::sources;
|
|
use super::sources::Sources;
|
|
use super::text;
|
|
use super::text::LineIndex;
|
|
use super::tsc;
|
|
use super::tsc::AssetDocument;
|
|
use super::tsc::Assets;
|
|
use super::tsc::TsServer;
|
|
use super::urls;
|
|
|
|
pub const REGISTRIES_PATH: &str = "registries";
|
|
const SOURCES_PATH: &str = "deps";
|
|
|
|
lazy_static::lazy_static! {
|
|
static ref ABSTRACT_MODIFIER: Regex = Regex::new(r"\babstract\b").unwrap();
|
|
static ref EXPORT_MODIFIER: Regex = Regex::new(r"\bexport\b").unwrap();
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct LanguageServer(Arc<tokio::sync::Mutex<Inner>>);
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct StateSnapshot {
|
|
pub assets: Assets,
|
|
pub config: Config,
|
|
pub documents: DocumentCache,
|
|
pub module_registries: registries::ModuleRegistry,
|
|
pub performance: Performance,
|
|
pub sources: Sources,
|
|
pub url_map: urls::LspUrlMap,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct Inner {
|
|
/// Cached versions of "fixed" assets that can either be inlined in Rust or
|
|
/// are part of the TypeScript snapshot and have to be fetched out.
|
|
assets: Assets,
|
|
/// The LSP client that this LSP server is connected to.
|
|
pub(crate) client: Client,
|
|
/// Configuration information.
|
|
config: Config,
|
|
diagnostics_server: diagnostics::DiagnosticsServer,
|
|
/// The "in-memory" documents in the editor which can be updated and changed.
|
|
documents: DocumentCache,
|
|
/// Handles module registries, which allow discovery of modules
|
|
module_registries: registries::ModuleRegistry,
|
|
/// The path to the module registries cache
|
|
module_registries_location: PathBuf,
|
|
/// An optional URL which provides the location of a TypeScript configuration
|
|
/// file which will be used by the Deno LSP.
|
|
maybe_config_uri: Option<Url>,
|
|
/// An optional import map which is used to resolve modules.
|
|
pub(crate) maybe_import_map: Option<ImportMap>,
|
|
/// The URL for the import map which is used to determine relative imports.
|
|
maybe_import_map_uri: Option<Url>,
|
|
/// A map of all the cached navigation trees.
|
|
navigation_trees: HashMap<ModuleSpecifier, tsc::NavigationTree>,
|
|
/// A collection of measurements which instrument that performance of the LSP.
|
|
performance: Performance,
|
|
/// Cached sources that are read-only.
|
|
sources: Sources,
|
|
/// A memoized version of fixable diagnostic codes retrieved from TypeScript.
|
|
ts_fixable_diagnostics: Vec<String>,
|
|
/// An abstraction that handles interactions with TypeScript.
|
|
pub(crate) ts_server: Arc<TsServer>,
|
|
/// A map of specifiers and URLs used to translate over the LSP.
|
|
pub(crate) url_map: urls::LspUrlMap,
|
|
}
|
|
|
|
impl LanguageServer {
|
|
pub fn new(client: Client) -> Self {
|
|
Self(Arc::new(tokio::sync::Mutex::new(Inner::new(client))))
|
|
}
|
|
}
|
|
|
|
impl Inner {
|
|
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 module_registries_location = dir.root.join(REGISTRIES_PATH);
|
|
let module_registries =
|
|
registries::ModuleRegistry::new(&module_registries_location);
|
|
let sources_location = dir.root.join(SOURCES_PATH);
|
|
let sources = Sources::new(&sources_location);
|
|
let ts_server = Arc::new(TsServer::new());
|
|
let performance = Performance::default();
|
|
let diagnostics_server = diagnostics::DiagnosticsServer::new();
|
|
|
|
Self {
|
|
assets: Default::default(),
|
|
client,
|
|
config: Default::default(),
|
|
diagnostics_server,
|
|
documents: Default::default(),
|
|
maybe_config_uri: Default::default(),
|
|
maybe_import_map: Default::default(),
|
|
maybe_import_map_uri: Default::default(),
|
|
module_registries,
|
|
module_registries_location,
|
|
navigation_trees: Default::default(),
|
|
performance,
|
|
sources,
|
|
ts_fixable_diagnostics: Default::default(),
|
|
ts_server,
|
|
url_map: Default::default(),
|
|
}
|
|
}
|
|
|
|
/// Analyzes dependencies of a document that has been opened in the editor and
|
|
/// sets the dependencies property on the document.
|
|
fn analyze_dependencies(
|
|
&mut self,
|
|
specifier: &ModuleSpecifier,
|
|
source: &str,
|
|
) {
|
|
let media_type = MediaType::from(specifier);
|
|
if let Ok(parsed_module) =
|
|
analysis::parse_module(specifier, source, &media_type)
|
|
{
|
|
let (mut deps, _) = analysis::analyze_dependencies(
|
|
specifier,
|
|
&media_type,
|
|
&parsed_module,
|
|
&self.maybe_import_map,
|
|
);
|
|
for (_, dep) in deps.iter_mut() {
|
|
if dep.maybe_type.is_none() {
|
|
if let Some(ResolvedDependency::Resolved(resolved)) = &dep.maybe_code
|
|
{
|
|
dep.maybe_type = self.sources.get_maybe_types(resolved);
|
|
}
|
|
}
|
|
}
|
|
if let Err(err) = self.documents.set_dependencies(specifier, Some(deps)) {
|
|
error!("{}", err);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Searches assets, open documents and external sources for a line_index,
|
|
/// which might be performed asynchronously, hydrating in memory caches for
|
|
/// subsequent requests.
|
|
pub(crate) async fn get_line_index(
|
|
&mut self,
|
|
specifier: ModuleSpecifier,
|
|
) -> Result<LineIndex, AnyError> {
|
|
let mark = self
|
|
.performance
|
|
.mark("get_line_index", Some(json!({ "specifier": specifier })));
|
|
let result = if specifier.scheme() == "asset" {
|
|
if let Some(asset) = self.get_asset(&specifier).await? {
|
|
Ok(asset.line_index)
|
|
} else {
|
|
Err(anyhow!("asset is missing: {}", specifier))
|
|
}
|
|
} else if let Some(line_index) = self.documents.line_index(&specifier) {
|
|
Ok(line_index)
|
|
} else if let Some(line_index) = self.sources.get_line_index(&specifier) {
|
|
Ok(line_index)
|
|
} else {
|
|
Err(anyhow!("Unable to find line index for: {}", specifier))
|
|
};
|
|
self.performance.measure(mark);
|
|
result
|
|
}
|
|
|
|
/// Only searches already cached assets and documents for a line index. If
|
|
/// the line index cannot be found, `None` is returned.
|
|
fn get_line_index_sync(
|
|
&self,
|
|
specifier: &ModuleSpecifier,
|
|
) -> Option<LineIndex> {
|
|
let mark = self.performance.mark(
|
|
"get_line_index_sync",
|
|
Some(json!({ "specifier": specifier })),
|
|
);
|
|
let maybe_line_index = if specifier.scheme() == "asset" {
|
|
if let Some(Some(asset)) = self.assets.get(specifier) {
|
|
Some(asset.line_index.clone())
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
let documents = &self.documents;
|
|
if documents.contains_key(specifier) {
|
|
documents.line_index(specifier)
|
|
} else {
|
|
self.sources.get_line_index(specifier)
|
|
}
|
|
};
|
|
self.performance.measure(mark);
|
|
maybe_line_index
|
|
}
|
|
|
|
// TODO(@kitsonk) we really should find a better way to just return the
|
|
// content as a `&str`, or be able to get the byte at a particular offset
|
|
// which is all that this API that is consuming it is trying to do at the
|
|
// moment
|
|
/// Searches already cached assets and documents and returns its text
|
|
/// content. If not found, `None` is returned.
|
|
fn get_text_content(&self, specifier: &ModuleSpecifier) -> Option<String> {
|
|
if specifier.scheme() == "asset" {
|
|
self
|
|
.assets
|
|
.get(specifier)
|
|
.map(|o| o.clone().map(|a| a.text))?
|
|
} else if self.documents.contains_key(specifier) {
|
|
self.documents.content(specifier).unwrap()
|
|
} else {
|
|
self.sources.get_source(specifier)
|
|
}
|
|
}
|
|
|
|
async fn get_navigation_tree(
|
|
&mut self,
|
|
specifier: &ModuleSpecifier,
|
|
) -> Result<tsc::NavigationTree, AnyError> {
|
|
let mark = self.performance.mark(
|
|
"get_navigation_tree",
|
|
Some(json!({ "specifier": specifier })),
|
|
);
|
|
if let Some(navigation_tree) = self.navigation_trees.get(specifier) {
|
|
self.performance.measure(mark);
|
|
Ok(navigation_tree.clone())
|
|
} else {
|
|
let navigation_tree: tsc::NavigationTree = self
|
|
.ts_server
|
|
.request(
|
|
self.snapshot(),
|
|
tsc::RequestMethod::GetNavigationTree(specifier.clone()),
|
|
)
|
|
.await?;
|
|
self
|
|
.navigation_trees
|
|
.insert(specifier.clone(), navigation_tree.clone());
|
|
self.performance.measure(mark);
|
|
Ok(navigation_tree)
|
|
}
|
|
}
|
|
|
|
pub(crate) fn snapshot(&self) -> StateSnapshot {
|
|
StateSnapshot {
|
|
assets: self.assets.clone(),
|
|
config: self.config.clone(),
|
|
documents: self.documents.clone(),
|
|
module_registries: self.module_registries.clone(),
|
|
performance: self.performance.clone(),
|
|
sources: self.sources.clone(),
|
|
url_map: self.url_map.clone(),
|
|
}
|
|
}
|
|
|
|
pub async fn update_import_map(&mut self) -> Result<(), AnyError> {
|
|
let mark = self.performance.mark("update_import_map", None::<()>);
|
|
let (maybe_import_map, maybe_root_uri) = {
|
|
let config = &self.config;
|
|
(
|
|
config.workspace_settings.import_map.clone(),
|
|
config.root_uri.clone(),
|
|
)
|
|
};
|
|
if let Some(import_map_str) = &maybe_import_map {
|
|
info!("Updating import map from: \"{}\"", import_map_str);
|
|
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 = Some(import_map_url);
|
|
self.maybe_import_map = Some(import_map);
|
|
} else {
|
|
self.maybe_import_map = None;
|
|
}
|
|
self.performance.measure(mark);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn update_debug_flag(&self) -> bool {
|
|
logger::LSP_DEBUG_FLAG
|
|
.compare_exchange(
|
|
!self.config.workspace_settings.internal_debug,
|
|
self.config.workspace_settings.internal_debug,
|
|
Ordering::Acquire,
|
|
Ordering::Relaxed,
|
|
)
|
|
.is_ok()
|
|
}
|
|
|
|
async fn update_registries(&mut self) -> Result<(), AnyError> {
|
|
let mark = self.performance.mark("update_registries", None::<()>);
|
|
for (registry, enabled) in
|
|
self.config.workspace_settings.suggest.imports.hosts.iter()
|
|
{
|
|
if *enabled {
|
|
info!("Enabling auto complete registry for: {}", registry);
|
|
self.module_registries.enable(registry).await?;
|
|
} else {
|
|
info!("Disabling auto complete registry for: {}", registry);
|
|
self.module_registries.disable(registry).await?;
|
|
}
|
|
}
|
|
self.performance.measure(mark);
|
|
Ok(())
|
|
}
|
|
|
|
async fn update_tsconfig(&mut self) -> Result<(), AnyError> {
|
|
let mark = self.performance.mark("update_tsconfig", None::<()>);
|
|
let mut tsconfig = TsConfig::new(json!({
|
|
"allowJs": true,
|
|
"esModuleInterop": true,
|
|
"experimentalDecorators": true,
|
|
"isolatedModules": true,
|
|
"jsx": "react",
|
|
"lib": ["deno.ns", "deno.window"],
|
|
"module": "esnext",
|
|
"noEmit": true,
|
|
"strict": true,
|
|
"target": "esnext",
|
|
"useDefineForClassFields": true,
|
|
}));
|
|
let (maybe_config, maybe_root_uri) = {
|
|
let config = &self.config;
|
|
if config.workspace_settings.unstable {
|
|
let unstable_libs = json!({
|
|
"lib": ["deno.ns", "deno.window", "deno.unstable"]
|
|
});
|
|
tsconfig.merge(&unstable_libs);
|
|
}
|
|
(
|
|
config.workspace_settings.config.clone(),
|
|
config.root_uri.clone(),
|
|
)
|
|
};
|
|
if let Some(config_str) = &maybe_config {
|
|
info!("Updating TypeScript configuration from: \"{}\"", config_str);
|
|
let config_url = if let Ok(url) = Url::from_file_path(config_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 config_path = root_path.join(config_str);
|
|
Url::from_file_path(config_path).map_err(|_| {
|
|
anyhow!("Bad file path for configuration file: \"{}\"", config_str)
|
|
})
|
|
} else {
|
|
Err(anyhow!(
|
|
"The path to the configuration file (\"{}\") is not resolvable.",
|
|
config_str
|
|
))
|
|
}?;
|
|
|
|
let config_file = ConfigFile::read(config_url.as_str())
|
|
.context("Failed to load configuration file")?;
|
|
let (value, maybe_ignored_options) = config_file.as_compiler_options()?;
|
|
tsconfig.merge(&value);
|
|
self.maybe_config_uri = Some(config_url);
|
|
if let Some(ignored_options) = maybe_ignored_options {
|
|
// TODO(@kitsonk) turn these into diagnostics that can be sent to the
|
|
// client
|
|
warn!("{}", ignored_options);
|
|
}
|
|
}
|
|
let _ok: bool = self
|
|
.ts_server
|
|
.request(self.snapshot(), tsc::RequestMethod::Configure(tsconfig))
|
|
.await?;
|
|
self.performance.measure(mark);
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn document_version(
|
|
&self,
|
|
specifier: ModuleSpecifier,
|
|
) -> Option<i32> {
|
|
self.documents.version(&specifier)
|
|
}
|
|
|
|
async fn get_asset(
|
|
&mut self,
|
|
specifier: &ModuleSpecifier,
|
|
) -> Result<Option<AssetDocument>, AnyError> {
|
|
if let Some(maybe_asset) = self.assets.get(specifier) {
|
|
return Ok(maybe_asset.clone());
|
|
} else {
|
|
let maybe_asset =
|
|
tsc::get_asset(&specifier, &self.ts_server, self.snapshot()).await?;
|
|
self.assets.insert(specifier.clone(), maybe_asset.clone());
|
|
Ok(maybe_asset)
|
|
}
|
|
}
|
|
}
|
|
|
|
// lspower::LanguageServer methods. This file's LanguageServer delegates to us.
|
|
impl Inner {
|
|
async fn initialize(
|
|
&mut self,
|
|
params: InitializeParams,
|
|
) -> LspResult<InitializeResult> {
|
|
info!("Starting Deno language server...");
|
|
let mark = self.performance.mark("initialize", Some(¶ms));
|
|
|
|
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 config = &mut self.config;
|
|
config.root_uri = params.root_uri;
|
|
if let Some(value) = params.initialization_options {
|
|
config.update_workspace(value)?;
|
|
}
|
|
config.update_capabilities(¶ms.capabilities);
|
|
}
|
|
|
|
self.update_debug_flag();
|
|
if let Err(err) = self.update_tsconfig().await {
|
|
warn!("Updating tsconfig has errored: {}", err);
|
|
}
|
|
|
|
if capabilities.code_action_provider.is_some() {
|
|
let fixable_diagnostics: Vec<String> = self
|
|
.ts_server
|
|
.request(self.snapshot(), tsc::RequestMethod::GetSupportedCodeFixes)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Unable to get fixable diagnostics: {}", err);
|
|
LspError::internal_error()
|
|
})?;
|
|
self.ts_fixable_diagnostics = fixable_diagnostics;
|
|
}
|
|
|
|
self.performance.measure(mark);
|
|
Ok(InitializeResult {
|
|
capabilities,
|
|
server_info: Some(server_info),
|
|
})
|
|
}
|
|
|
|
async fn initialized(&mut 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;
|
|
}
|
|
// Check to see if we need to setup any module registries
|
|
if let Err(err) = self.update_registries().await {
|
|
self
|
|
.client
|
|
.show_message(MessageType::Warning, err.to_string())
|
|
.await;
|
|
}
|
|
|
|
if self
|
|
.config
|
|
.client_capabilities
|
|
.workspace_did_change_watched_files
|
|
{
|
|
// 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(&mut self, params: DidOpenTextDocumentParams) {
|
|
let mark = self.performance.mark("did_open", Some(¶ms));
|
|
let specifier = self.url_map.normalize_url(¶ms.text_document.uri);
|
|
|
|
// we only query the individual resource file if the client supports it
|
|
// TODO(@kitsonk) workaround https://github.com/denoland/deno/issues/10603
|
|
// if self.config.client_capabilities.workspace_configuration
|
|
// && !self.config.contains(&specifier)
|
|
// {
|
|
// if let Ok(value) = self
|
|
// .client
|
|
// .configuration(vec![ConfigurationItem {
|
|
// scope_uri: Some(params.text_document.uri.clone()),
|
|
// section: Some(SETTINGS_SECTION.to_string()),
|
|
// }])
|
|
// .await
|
|
// {
|
|
// if let Err(err) = self
|
|
// .config
|
|
// .update_specifier(specifier.clone(), value[0].clone())
|
|
// {
|
|
// warn!("Error updating specifier configuration: {}", err);
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
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;
|
|
}
|
|
self.documents.open(
|
|
specifier.clone(),
|
|
params.text_document.version,
|
|
¶ms.text_document.text,
|
|
);
|
|
self.analyze_dependencies(&specifier, ¶ms.text_document.text);
|
|
self.performance.measure(mark);
|
|
|
|
if let Err(err) = self.diagnostics_server.update() {
|
|
error!("{}", err);
|
|
}
|
|
}
|
|
|
|
async fn did_change(&mut self, params: DidChangeTextDocumentParams) {
|
|
let mark = self.performance.mark("did_change", Some(¶ms));
|
|
let specifier = self.url_map.normalize_url(¶ms.text_document.uri);
|
|
match self.documents.change(
|
|
&specifier,
|
|
params.text_document.version,
|
|
params.content_changes,
|
|
) {
|
|
Ok(Some(source)) => self.analyze_dependencies(&specifier, &source),
|
|
Ok(_) => error!("No content returned from change."),
|
|
Err(err) => error!("{}", err),
|
|
}
|
|
self.performance.measure(mark);
|
|
|
|
if let Err(err) = self.diagnostics_server.update() {
|
|
error!("{}", err);
|
|
}
|
|
}
|
|
|
|
async fn did_close(&mut self, params: DidCloseTextDocumentParams) {
|
|
let mark = self.performance.mark("did_close", Some(¶ms));
|
|
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 = self.url_map.normalize_url(¶ms.text_document.uri);
|
|
self.documents.close(&specifier);
|
|
self.navigation_trees.remove(&specifier);
|
|
|
|
self.performance.measure(mark);
|
|
if let Err(err) = self.diagnostics_server.update() {
|
|
error!("{}", err);
|
|
}
|
|
}
|
|
|
|
async fn did_change_configuration(
|
|
&mut self,
|
|
params: DidChangeConfigurationParams,
|
|
) {
|
|
let mark = self
|
|
.performance
|
|
.mark("did_change_configuration", Some(¶ms));
|
|
|
|
if self.config.client_capabilities.workspace_configuration {
|
|
let specifiers: Vec<ModuleSpecifier> =
|
|
self.config.specifier_settings.keys().cloned().collect();
|
|
let mut snapshot = self.snapshot();
|
|
let mut config_items = specifiers
|
|
.iter()
|
|
.map(|s| ConfigurationItem {
|
|
scope_uri: Some(snapshot.url_map.normalize_specifier(s).unwrap()),
|
|
section: Some(SETTINGS_SECTION.to_string()),
|
|
})
|
|
.collect();
|
|
let mut items = vec![ConfigurationItem {
|
|
scope_uri: None,
|
|
section: Some(SETTINGS_SECTION.to_string()),
|
|
}];
|
|
items.append(&mut config_items);
|
|
if let Ok(configs) = self.client.configuration(items).await {
|
|
for (i, value) in configs.into_iter().enumerate() {
|
|
if let Err(err) = match i {
|
|
0 => self.config.update_workspace(value),
|
|
_ => self
|
|
.config
|
|
.update_specifier(specifiers[i - 1].clone(), value),
|
|
} {
|
|
error!("failed to update settings: {}", err);
|
|
}
|
|
}
|
|
}
|
|
} else if let Some(config) = params
|
|
.settings
|
|
.as_object()
|
|
.map(|settings| settings.get(SETTINGS_SECTION))
|
|
.flatten()
|
|
.cloned()
|
|
{
|
|
if let Err(err) = self.config.update_workspace(config) {
|
|
error!("failed to update settings: {}", err);
|
|
}
|
|
}
|
|
|
|
self.update_debug_flag();
|
|
if let Err(err) = self.update_import_map().await {
|
|
self
|
|
.client
|
|
.show_message(MessageType::Warning, err.to_string())
|
|
.await;
|
|
}
|
|
if let Err(err) = self.update_registries().await {
|
|
self
|
|
.client
|
|
.show_message(MessageType::Warning, err.to_string())
|
|
.await;
|
|
}
|
|
if let Err(err) = self.update_tsconfig().await {
|
|
self
|
|
.client
|
|
.show_message(MessageType::Warning, err.to_string())
|
|
.await;
|
|
}
|
|
if let Err(err) = self.diagnostics_server.update() {
|
|
error!("{}", err);
|
|
}
|
|
|
|
self.performance.measure(mark);
|
|
}
|
|
|
|
async fn did_change_watched_files(
|
|
&mut self,
|
|
params: DidChangeWatchedFilesParams,
|
|
) {
|
|
let mark = self
|
|
.performance
|
|
.mark("did_change_watched_files", Some(¶ms));
|
|
// if the current import map has changed, we need to reload it
|
|
if let Some(import_map_uri) = &self.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;
|
|
}
|
|
}
|
|
}
|
|
// if the current tsconfig has changed, we need to reload it
|
|
if let Some(config_uri) = &self.maybe_config_uri {
|
|
if params.changes.iter().any(|fe| *config_uri == fe.uri) {
|
|
if let Err(err) = self.update_tsconfig().await {
|
|
self
|
|
.client
|
|
.show_message(MessageType::Warning, err.to_string())
|
|
.await;
|
|
}
|
|
}
|
|
}
|
|
self.performance.measure(mark);
|
|
}
|
|
|
|
async fn document_symbol(
|
|
&mut self,
|
|
params: DocumentSymbolParams,
|
|
) -> LspResult<Option<DocumentSymbolResponse>> {
|
|
let specifier = self.url_map.normalize_url(¶ms.text_document.uri);
|
|
if !self.config.specifier_enabled(&specifier) {
|
|
return Ok(None);
|
|
}
|
|
let mark = self.performance.mark("document_symbol", Some(¶ms));
|
|
|
|
let line_index =
|
|
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
|
line_index
|
|
} else {
|
|
return Err(LspError::invalid_params(format!(
|
|
"An unexpected specifier ({}) was provided.",
|
|
specifier
|
|
)));
|
|
};
|
|
|
|
let req = tsc::RequestMethod::GetNavigationTree(specifier);
|
|
let navigation_tree: tsc::NavigationTree = self
|
|
.ts_server
|
|
.request(self.snapshot(), req)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Failed to request to tsserver {}", err);
|
|
LspError::invalid_request()
|
|
})?;
|
|
|
|
let response = if let Some(child_items) = navigation_tree.child_items {
|
|
let mut document_symbols = Vec::<DocumentSymbol>::new();
|
|
for item in child_items {
|
|
item.collect_document_symbols(&line_index, &mut document_symbols);
|
|
}
|
|
Some(DocumentSymbolResponse::Nested(document_symbols))
|
|
} else {
|
|
None
|
|
};
|
|
self.performance.measure(mark);
|
|
Ok(response)
|
|
}
|
|
|
|
async fn formatting(
|
|
&self,
|
|
params: DocumentFormattingParams,
|
|
) -> LspResult<Option<Vec<TextEdit>>> {
|
|
let mark = self.performance.mark("formatting", Some(¶ms));
|
|
let specifier = self.url_map.normalize_url(¶ms.text_document.uri);
|
|
let file_text = self
|
|
.documents
|
|
.content(&specifier)
|
|
.map_err(|_| {
|
|
LspError::invalid_params(
|
|
"The specified file could not be found in memory.",
|
|
)
|
|
})?
|
|
.unwrap();
|
|
let line_index = self.documents.line_index(&specifier);
|
|
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, line_index))
|
|
}
|
|
Err(err) => {
|
|
warn!("Format error: {}", err);
|
|
None
|
|
}
|
|
}
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
self.performance.measure(mark);
|
|
if let Some(text_edits) = text_edits {
|
|
if text_edits.is_empty() {
|
|
Ok(None)
|
|
} else {
|
|
Ok(Some(text_edits))
|
|
}
|
|
} else {
|
|
self.client.show_message(MessageType::Warning, format!("Unable to format \"{}\". Likely due to unrecoverable syntax errors in the file.", specifier)).await;
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
async fn hover(&mut self, params: HoverParams) -> LspResult<Option<Hover>> {
|
|
let specifier = self
|
|
.url_map
|
|
.normalize_url(¶ms.text_document_position_params.text_document.uri);
|
|
if !self.config.specifier_enabled(&specifier) {
|
|
return Ok(None);
|
|
}
|
|
let mark = self.performance.mark("hover", Some(¶ms));
|
|
|
|
let line_index =
|
|
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
|
line_index
|
|
} else {
|
|
return Err(LspError::invalid_params(format!(
|
|
"An unexpected specifier ({}) was provided.",
|
|
specifier
|
|
)));
|
|
};
|
|
let req = tsc::RequestMethod::GetQuickInfo((
|
|
specifier,
|
|
line_index.offset_tsc(params.text_document_position_params.position)?,
|
|
));
|
|
let maybe_quick_info: Option<tsc::QuickInfo> = self
|
|
.ts_server
|
|
.request(self.snapshot(), req)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Unable to get quick info: {}", err);
|
|
LspError::internal_error()
|
|
})?;
|
|
if let Some(quick_info) = maybe_quick_info {
|
|
let hover = quick_info.to_hover(&line_index);
|
|
self.performance.measure(mark);
|
|
Ok(Some(hover))
|
|
} else {
|
|
self.performance.measure(mark);
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
async fn code_action(
|
|
&mut self,
|
|
params: CodeActionParams,
|
|
) -> LspResult<Option<CodeActionResponse>> {
|
|
let specifier = self.url_map.normalize_url(¶ms.text_document.uri);
|
|
if !self.config.specifier_enabled(&specifier) {
|
|
return Ok(None);
|
|
}
|
|
|
|
let mark = self.performance.mark("code_action", Some(¶ms));
|
|
let fixable_diagnostics: Vec<&Diagnostic> = params
|
|
.context
|
|
.diagnostics
|
|
.iter()
|
|
.filter(|d| match &d.source {
|
|
Some(source) => match source.as_str() {
|
|
"deno-ts" => match &d.code {
|
|
Some(NumberOrString::String(code)) => {
|
|
self.ts_fixable_diagnostics.contains(code)
|
|
}
|
|
Some(NumberOrString::Number(code)) => {
|
|
self.ts_fixable_diagnostics.contains(&code.to_string())
|
|
}
|
|
_ => false,
|
|
},
|
|
"deno" => match &d.code {
|
|
Some(NumberOrString::String(code)) => {
|
|
code == "no-cache" || code == "no-cache-data"
|
|
}
|
|
_ => false,
|
|
},
|
|
_ => false,
|
|
},
|
|
None => false,
|
|
})
|
|
.collect();
|
|
if fixable_diagnostics.is_empty() {
|
|
self.performance.measure(mark);
|
|
return Ok(None);
|
|
}
|
|
let line_index = self.get_line_index_sync(&specifier).unwrap();
|
|
let mut code_actions = CodeActionCollection::default();
|
|
let file_diagnostics = self
|
|
.diagnostics_server
|
|
.get(&specifier, DiagnosticSource::TypeScript)
|
|
.await;
|
|
for diagnostic in &fixable_diagnostics {
|
|
match diagnostic.source.as_deref() {
|
|
Some("deno-ts") => {
|
|
let code = match diagnostic.code.as_ref().unwrap() {
|
|
NumberOrString::String(code) => code.to_string(),
|
|
NumberOrString::Number(code) => code.to_string(),
|
|
};
|
|
let codes = vec![code];
|
|
let req = tsc::RequestMethod::GetCodeFixes((
|
|
specifier.clone(),
|
|
line_index.offset_tsc(diagnostic.range.start)?,
|
|
line_index.offset_tsc(diagnostic.range.end)?,
|
|
codes,
|
|
));
|
|
let actions: Vec<tsc::CodeFixAction> =
|
|
match self.ts_server.request(self.snapshot(), req).await {
|
|
Ok(items) => items,
|
|
Err(err) => {
|
|
// sometimes tsc reports errors when retrieving code actions
|
|
// because they don't reflect the current state of the document
|
|
// so we will log them to the output, but we won't send an error
|
|
// message back to the client.
|
|
error!("Error getting actions from TypeScript: {}", err);
|
|
Vec::new()
|
|
}
|
|
};
|
|
for action in actions {
|
|
code_actions
|
|
.add_ts_fix_action(&action, diagnostic, self)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Unable to convert fix: {}", err);
|
|
LspError::internal_error()
|
|
})?;
|
|
if code_actions.is_fix_all_action(
|
|
&action,
|
|
diagnostic,
|
|
&file_diagnostics,
|
|
) {
|
|
code_actions
|
|
.add_ts_fix_all_action(&action, &specifier, diagnostic);
|
|
}
|
|
}
|
|
}
|
|
Some("deno") => {
|
|
code_actions
|
|
.add_deno_fix_action(diagnostic)
|
|
.map_err(|err| {
|
|
error!("{}", err);
|
|
LspError::internal_error()
|
|
})?
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
code_actions.set_preferred_fixes();
|
|
let code_action_response = code_actions.get_response();
|
|
self.performance.measure(mark);
|
|
Ok(Some(code_action_response))
|
|
}
|
|
|
|
async fn code_action_resolve(
|
|
&mut self,
|
|
params: CodeAction,
|
|
) -> LspResult<CodeAction> {
|
|
let mark = self.performance.mark("code_action_resolve", Some(¶ms));
|
|
let result = if let Some(data) = params.data.clone() {
|
|
let code_action_data: CodeActionData =
|
|
from_value(data).map_err(|err| {
|
|
error!("Unable to decode code action data: {}", err);
|
|
LspError::invalid_params("The CodeAction's data is invalid.")
|
|
})?;
|
|
let req = tsc::RequestMethod::GetCombinedCodeFix((
|
|
code_action_data.specifier,
|
|
json!(code_action_data.fix_id.clone()),
|
|
));
|
|
let combined_code_actions: tsc::CombinedCodeActions = self
|
|
.ts_server
|
|
.request(self.snapshot(), req)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Unable to get combined fix from TypeScript: {}", err);
|
|
LspError::internal_error()
|
|
})?;
|
|
if combined_code_actions.commands.is_some() {
|
|
error!("Deno does not support code actions with commands.");
|
|
Err(LspError::invalid_request())
|
|
} else {
|
|
let mut code_action = params.clone();
|
|
code_action.edit =
|
|
ts_changes_to_edit(&combined_code_actions.changes, self)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Unable to convert changes to edits: {}", err);
|
|
LspError::internal_error()
|
|
})?;
|
|
Ok(code_action)
|
|
}
|
|
} else {
|
|
// The code action doesn't need to be resolved
|
|
Ok(params)
|
|
};
|
|
self.performance.measure(mark);
|
|
result
|
|
}
|
|
|
|
async fn code_lens(
|
|
&mut self,
|
|
params: CodeLensParams,
|
|
) -> LspResult<Option<Vec<CodeLens>>> {
|
|
let specifier = self.url_map.normalize_url(¶ms.text_document.uri);
|
|
if !self.config.specifier_enabled(&specifier)
|
|
|| !self.config.workspace_settings.enabled_code_lens()
|
|
{
|
|
return Ok(None);
|
|
}
|
|
|
|
let mark = self.performance.mark("code_lens", Some(¶ms));
|
|
let line_index = self.get_line_index_sync(&specifier).unwrap();
|
|
let navigation_tree =
|
|
self.get_navigation_tree(&specifier).await.map_err(|err| {
|
|
error!("Failed to retrieve nav tree: {}", err);
|
|
LspError::invalid_request()
|
|
})?;
|
|
|
|
// because we have to use this as a mutable in a closure, the compiler
|
|
// can't be sure when the vector will be mutated, and so a RefCell is
|
|
// required to "protect" the vector.
|
|
let cl = Rc::new(RefCell::new(Vec::new()));
|
|
navigation_tree.walk(&|i, mp| {
|
|
let mut code_lenses = cl.borrow_mut();
|
|
|
|
// TSC Implementations Code Lens
|
|
if self.config.workspace_settings.code_lens.implementations {
|
|
let source = CodeLensSource::Implementations;
|
|
match i.kind {
|
|
tsc::ScriptElementKind::InterfaceElement => {
|
|
code_lenses.push(i.to_code_lens(&line_index, &specifier, &source));
|
|
}
|
|
tsc::ScriptElementKind::ClassElement
|
|
| tsc::ScriptElementKind::MemberFunctionElement
|
|
| tsc::ScriptElementKind::MemberVariableElement
|
|
| tsc::ScriptElementKind::MemberGetAccessorElement
|
|
| tsc::ScriptElementKind::MemberSetAccessorElement => {
|
|
if ABSTRACT_MODIFIER.is_match(&i.kind_modifiers) {
|
|
code_lenses.push(i.to_code_lens(
|
|
&line_index,
|
|
&specifier,
|
|
&source,
|
|
));
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
// TSC References Code Lens
|
|
if self.config.workspace_settings.code_lens.references {
|
|
let source = CodeLensSource::References;
|
|
if let Some(parent) = &mp {
|
|
if parent.kind == tsc::ScriptElementKind::EnumElement {
|
|
code_lenses.push(i.to_code_lens(&line_index, &specifier, &source));
|
|
}
|
|
}
|
|
match i.kind {
|
|
tsc::ScriptElementKind::FunctionElement => {
|
|
if self
|
|
.config
|
|
.workspace_settings
|
|
.code_lens
|
|
.references_all_functions
|
|
{
|
|
code_lenses.push(i.to_code_lens(
|
|
&line_index,
|
|
&specifier,
|
|
&source,
|
|
));
|
|
}
|
|
}
|
|
tsc::ScriptElementKind::ConstElement
|
|
| tsc::ScriptElementKind::LetElement
|
|
| tsc::ScriptElementKind::VariableElement => {
|
|
if EXPORT_MODIFIER.is_match(&i.kind_modifiers) {
|
|
code_lenses.push(i.to_code_lens(
|
|
&line_index,
|
|
&specifier,
|
|
&source,
|
|
));
|
|
}
|
|
}
|
|
tsc::ScriptElementKind::ClassElement => {
|
|
if i.text != "<class>" {
|
|
code_lenses.push(i.to_code_lens(
|
|
&line_index,
|
|
&specifier,
|
|
&source,
|
|
));
|
|
}
|
|
}
|
|
tsc::ScriptElementKind::InterfaceElement
|
|
| tsc::ScriptElementKind::TypeElement
|
|
| tsc::ScriptElementKind::EnumElement => {
|
|
code_lenses.push(i.to_code_lens(&line_index, &specifier, &source));
|
|
}
|
|
tsc::ScriptElementKind::LocalFunctionElement
|
|
| tsc::ScriptElementKind::MemberGetAccessorElement
|
|
| tsc::ScriptElementKind::MemberSetAccessorElement
|
|
| tsc::ScriptElementKind::ConstructorImplementationElement
|
|
| tsc::ScriptElementKind::MemberVariableElement => {
|
|
if let Some(parent) = &mp {
|
|
if parent.spans[0].start != i.spans[0].start {
|
|
match parent.kind {
|
|
tsc::ScriptElementKind::ClassElement
|
|
| tsc::ScriptElementKind::InterfaceElement
|
|
| tsc::ScriptElementKind::TypeElement => {
|
|
code_lenses.push(i.to_code_lens(
|
|
&line_index,
|
|
&specifier,
|
|
&source,
|
|
));
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
});
|
|
|
|
self.performance.measure(mark);
|
|
Ok(Some(Rc::try_unwrap(cl).unwrap().into_inner()))
|
|
}
|
|
|
|
async fn code_lens_resolve(
|
|
&mut self,
|
|
params: CodeLens,
|
|
) -> LspResult<CodeLens> {
|
|
let mark = self.performance.mark("code_lens_resolve", Some(¶ms));
|
|
if let Some(data) = params.data.clone() {
|
|
let code_lens_data: CodeLensData = serde_json::from_value(data)
|
|
.map_err(|err| LspError::invalid_params(err.to_string()))?;
|
|
let code_lens = match code_lens_data.source {
|
|
CodeLensSource::Implementations => {
|
|
let line_index =
|
|
self.get_line_index_sync(&code_lens_data.specifier).unwrap();
|
|
let req = tsc::RequestMethod::GetImplementation((
|
|
code_lens_data.specifier.clone(),
|
|
line_index.offset_tsc(params.range.start)?,
|
|
));
|
|
let maybe_implementations: Option<Vec<tsc::ImplementationLocation>> =
|
|
self.ts_server.request(self.snapshot(), req).await.map_err(
|
|
|err| {
|
|
error!("Error processing TypeScript request: {}", err);
|
|
LspError::internal_error()
|
|
},
|
|
)?;
|
|
if let Some(implementations) = maybe_implementations {
|
|
let mut locations = Vec::new();
|
|
for implementation in implementations {
|
|
let implementation_specifier = resolve_url(
|
|
&implementation.document_span.file_name,
|
|
)
|
|
.map_err(|err| {
|
|
error!("Invalid specifier returned from TypeScript: {}", err);
|
|
LspError::internal_error()
|
|
})?;
|
|
let implementation_location =
|
|
implementation.to_location(&line_index, self);
|
|
if !(implementation_specifier == code_lens_data.specifier
|
|
&& implementation_location.range.start == params.range.start)
|
|
{
|
|
locations.push(implementation_location);
|
|
}
|
|
}
|
|
let command = if !locations.is_empty() {
|
|
let title = if locations.len() > 1 {
|
|
format!("{} implementations", locations.len())
|
|
} else {
|
|
"1 implementation".to_string()
|
|
};
|
|
let url = self
|
|
.url_map
|
|
.normalize_specifier(&code_lens_data.specifier)
|
|
.map_err(|err| {
|
|
error!("{}", err);
|
|
LspError::internal_error()
|
|
})?;
|
|
Command {
|
|
title,
|
|
command: "deno.showReferences".to_string(),
|
|
arguments: Some(vec![
|
|
serde_json::to_value(url).unwrap(),
|
|
serde_json::to_value(params.range.start).unwrap(),
|
|
serde_json::to_value(locations).unwrap(),
|
|
]),
|
|
}
|
|
} else {
|
|
Command {
|
|
title: "0 implementations".to_string(),
|
|
command: "".to_string(),
|
|
arguments: None,
|
|
}
|
|
};
|
|
CodeLens {
|
|
range: params.range,
|
|
command: Some(command),
|
|
data: None,
|
|
}
|
|
} else {
|
|
let command = Command {
|
|
title: "0 implementations".to_string(),
|
|
command: "".to_string(),
|
|
arguments: None,
|
|
};
|
|
CodeLens {
|
|
range: params.range,
|
|
command: Some(command),
|
|
data: None,
|
|
}
|
|
}
|
|
}
|
|
CodeLensSource::References => {
|
|
let line_index =
|
|
self.get_line_index_sync(&code_lens_data.specifier).unwrap();
|
|
let req = tsc::RequestMethod::GetReferences((
|
|
code_lens_data.specifier.clone(),
|
|
line_index.offset_tsc(params.range.start)?,
|
|
));
|
|
let maybe_references: Option<Vec<tsc::ReferenceEntry>> =
|
|
self.ts_server.request(self.snapshot(), req).await.map_err(
|
|
|err| {
|
|
error!("Error processing TypeScript request: {}", err);
|
|
LspError::internal_error()
|
|
},
|
|
)?;
|
|
if let Some(references) = maybe_references {
|
|
let mut locations = Vec::new();
|
|
for reference in references {
|
|
if reference.is_definition {
|
|
continue;
|
|
}
|
|
let reference_specifier = resolve_url(
|
|
&reference.document_span.file_name,
|
|
)
|
|
.map_err(|err| {
|
|
error!("Invalid specifier returned from TypeScript: {}", err);
|
|
LspError::internal_error()
|
|
})?;
|
|
let line_index = self
|
|
.get_line_index(reference_specifier)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Unable to get line index: {}", err);
|
|
LspError::internal_error()
|
|
})?;
|
|
locations.push(reference.to_location(&line_index, self));
|
|
}
|
|
let command = if !locations.is_empty() {
|
|
let title = if locations.len() > 1 {
|
|
format!("{} references", locations.len())
|
|
} else {
|
|
"1 reference".to_string()
|
|
};
|
|
let url = self
|
|
.url_map
|
|
.normalize_specifier(&code_lens_data.specifier)
|
|
.map_err(|err| {
|
|
error!("{}", err);
|
|
LspError::internal_error()
|
|
})?;
|
|
Command {
|
|
title,
|
|
command: "deno.showReferences".to_string(),
|
|
arguments: Some(vec![
|
|
serde_json::to_value(url).unwrap(),
|
|
serde_json::to_value(params.range.start).unwrap(),
|
|
serde_json::to_value(locations).unwrap(),
|
|
]),
|
|
}
|
|
} else {
|
|
Command {
|
|
title: "0 references".to_string(),
|
|
command: "".to_string(),
|
|
arguments: None,
|
|
}
|
|
};
|
|
CodeLens {
|
|
range: params.range,
|
|
command: Some(command),
|
|
data: None,
|
|
}
|
|
} else {
|
|
let command = Command {
|
|
title: "0 references".to_string(),
|
|
command: "".to_string(),
|
|
arguments: None,
|
|
};
|
|
CodeLens {
|
|
range: params.range,
|
|
command: Some(command),
|
|
data: None,
|
|
}
|
|
}
|
|
}
|
|
};
|
|
self.performance.measure(mark);
|
|
Ok(code_lens)
|
|
} else {
|
|
self.performance.measure(mark);
|
|
Err(LspError::invalid_params(
|
|
"Code lens is missing the \"data\" property.",
|
|
))
|
|
}
|
|
}
|
|
|
|
async fn document_highlight(
|
|
&mut self,
|
|
params: DocumentHighlightParams,
|
|
) -> LspResult<Option<Vec<DocumentHighlight>>> {
|
|
let specifier = self
|
|
.url_map
|
|
.normalize_url(¶ms.text_document_position_params.text_document.uri);
|
|
if !self.config.specifier_enabled(&specifier) {
|
|
return Ok(None);
|
|
}
|
|
|
|
let mark = self.performance.mark("document_highlight", Some(¶ms));
|
|
let line_index =
|
|
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
|
line_index
|
|
} else {
|
|
return Err(LspError::invalid_params(format!(
|
|
"An unexpected specifier ({}) was provided.",
|
|
specifier
|
|
)));
|
|
};
|
|
let files_to_search = vec![specifier.clone()];
|
|
let req = tsc::RequestMethod::GetDocumentHighlights((
|
|
specifier,
|
|
line_index.offset_tsc(params.text_document_position_params.position)?,
|
|
files_to_search,
|
|
));
|
|
let maybe_document_highlights: Option<Vec<tsc::DocumentHighlights>> = self
|
|
.ts_server
|
|
.request(self.snapshot(), req)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Unable to get document highlights from TypeScript: {}", err);
|
|
LspError::internal_error()
|
|
})?;
|
|
|
|
if let Some(document_highlights) = maybe_document_highlights {
|
|
let result = document_highlights
|
|
.into_iter()
|
|
.map(|dh| dh.to_highlight(&line_index))
|
|
.flatten()
|
|
.collect();
|
|
self.performance.measure(mark);
|
|
Ok(Some(result))
|
|
} else {
|
|
self.performance.measure(mark);
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
async fn references(
|
|
&mut self,
|
|
params: ReferenceParams,
|
|
) -> LspResult<Option<Vec<Location>>> {
|
|
let specifier = self
|
|
.url_map
|
|
.normalize_url(¶ms.text_document_position.text_document.uri);
|
|
if !self.config.specifier_enabled(&specifier) {
|
|
return Ok(None);
|
|
}
|
|
let mark = self.performance.mark("references", Some(¶ms));
|
|
let line_index =
|
|
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
|
line_index
|
|
} else {
|
|
return Err(LspError::invalid_params(format!(
|
|
"An unexpected specifier ({}) was provided.",
|
|
specifier
|
|
)));
|
|
};
|
|
let req = tsc::RequestMethod::GetReferences((
|
|
specifier,
|
|
line_index.offset_tsc(params.text_document_position.position)?,
|
|
));
|
|
let maybe_references: Option<Vec<tsc::ReferenceEntry>> = self
|
|
.ts_server
|
|
.request(self.snapshot(), req)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Unable to get references from TypeScript: {}", err);
|
|
LspError::internal_error()
|
|
})?;
|
|
|
|
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 =
|
|
resolve_url(&reference.document_span.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, self));
|
|
}
|
|
|
|
self.performance.measure(mark);
|
|
Ok(Some(results))
|
|
} else {
|
|
self.performance.measure(mark);
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
async fn goto_definition(
|
|
&mut self,
|
|
params: GotoDefinitionParams,
|
|
) -> LspResult<Option<GotoDefinitionResponse>> {
|
|
let specifier = self
|
|
.url_map
|
|
.normalize_url(¶ms.text_document_position_params.text_document.uri);
|
|
if !self.config.specifier_enabled(&specifier) {
|
|
return Ok(None);
|
|
}
|
|
let mark = self.performance.mark("goto_definition", Some(¶ms));
|
|
let line_index =
|
|
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
|
line_index
|
|
} else {
|
|
return Err(LspError::invalid_params(format!(
|
|
"An unexpected specifier ({}) was provided.",
|
|
specifier
|
|
)));
|
|
};
|
|
let req = tsc::RequestMethod::GetDefinition((
|
|
specifier,
|
|
line_index.offset_tsc(params.text_document_position_params.position)?,
|
|
));
|
|
let maybe_definition: Option<tsc::DefinitionInfoAndBoundSpan> = self
|
|
.ts_server
|
|
.request(self.snapshot(), req)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Unable to get definition from TypeScript: {}", err);
|
|
LspError::internal_error()
|
|
})?;
|
|
|
|
if let Some(definition) = maybe_definition {
|
|
let results = definition.to_definition(&line_index, self).await;
|
|
self.performance.measure(mark);
|
|
Ok(results)
|
|
} else {
|
|
self.performance.measure(mark);
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
async fn completion(
|
|
&mut self,
|
|
params: CompletionParams,
|
|
) -> LspResult<Option<CompletionResponse>> {
|
|
let specifier = self
|
|
.url_map
|
|
.normalize_url(¶ms.text_document_position.text_document.uri);
|
|
if !self.config.specifier_enabled(&specifier) {
|
|
return Ok(None);
|
|
}
|
|
let mark = self.performance.mark("completion", Some(¶ms));
|
|
// Import specifiers are something wholly internal to Deno, so for
|
|
// completions, we will use internal logic and if there are completions
|
|
// for imports, we will return those and not send a message into tsc, where
|
|
// other completions come from.
|
|
let response = if let Some(response) = completions::get_import_completions(
|
|
&specifier,
|
|
¶ms.text_document_position.position,
|
|
&self.snapshot(),
|
|
)
|
|
.await
|
|
{
|
|
Some(response)
|
|
} else {
|
|
let line_index =
|
|
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
|
line_index
|
|
} else {
|
|
return Err(LspError::invalid_params(format!(
|
|
"An unexpected specifier ({}) was provided.",
|
|
specifier
|
|
)));
|
|
};
|
|
let trigger_character = if let Some(context) = ¶ms.context {
|
|
context.trigger_character.clone()
|
|
} else {
|
|
None
|
|
};
|
|
let position =
|
|
line_index.offset_tsc(params.text_document_position.position)?;
|
|
let req = tsc::RequestMethod::GetCompletions((
|
|
specifier.clone(),
|
|
position,
|
|
tsc::GetCompletionsAtPositionOptions {
|
|
user_preferences: tsc::UserPreferences {
|
|
include_completions_with_insert_text: Some(true),
|
|
..Default::default()
|
|
},
|
|
trigger_character,
|
|
},
|
|
));
|
|
let maybe_completion_info: Option<tsc::CompletionInfo> = self
|
|
.ts_server
|
|
.request(self.snapshot(), req)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Unable to get completion info from TypeScript: {}", err);
|
|
LspError::internal_error()
|
|
})?;
|
|
|
|
if let Some(completions) = maybe_completion_info {
|
|
let results = completions.as_completion_response(
|
|
&line_index,
|
|
&self.config.workspace_settings.suggest,
|
|
&specifier,
|
|
position,
|
|
);
|
|
Some(results)
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
self.performance.measure(mark);
|
|
Ok(response)
|
|
}
|
|
|
|
async fn completion_resolve(
|
|
&mut self,
|
|
params: CompletionItem,
|
|
) -> LspResult<CompletionItem> {
|
|
let mark = self.performance.mark("completion_resolve", Some(¶ms));
|
|
let completion_item = if let Some(data) = ¶ms.data {
|
|
let data: completions::CompletionItemData =
|
|
serde_json::from_value(data.clone()).map_err(|err| {
|
|
error!("{}", err);
|
|
LspError::invalid_params(
|
|
"Could not decode data field of completion item.",
|
|
)
|
|
})?;
|
|
if let Some(data) = data.tsc {
|
|
let req = tsc::RequestMethod::GetCompletionDetails(data.into());
|
|
let maybe_completion_info: Option<tsc::CompletionEntryDetails> =
|
|
self.ts_server.request(self.snapshot(), req).await.map_err(
|
|
|err| {
|
|
error!("Unable to get completion info from TypeScript: {}", err);
|
|
LspError::internal_error()
|
|
},
|
|
)?;
|
|
if let Some(completion_info) = maybe_completion_info {
|
|
completion_info.as_completion_item(¶ms)
|
|
} else {
|
|
error!(
|
|
"Received an undefined response from tsc for completion details."
|
|
);
|
|
params
|
|
}
|
|
} else {
|
|
params
|
|
}
|
|
} else {
|
|
params
|
|
};
|
|
self.performance.measure(mark);
|
|
Ok(completion_item)
|
|
}
|
|
|
|
async fn goto_implementation(
|
|
&mut self,
|
|
params: GotoImplementationParams,
|
|
) -> LspResult<Option<GotoImplementationResponse>> {
|
|
let specifier = self
|
|
.url_map
|
|
.normalize_url(¶ms.text_document_position_params.text_document.uri);
|
|
if !self.config.specifier_enabled(&specifier) {
|
|
return Ok(None);
|
|
}
|
|
let mark = self.performance.mark("goto_implementation", Some(¶ms));
|
|
let line_index =
|
|
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
|
line_index
|
|
} else {
|
|
return Err(LspError::invalid_params(format!(
|
|
"An unexpected specifier ({}) was provided.",
|
|
specifier
|
|
)));
|
|
};
|
|
|
|
let req = tsc::RequestMethod::GetImplementation((
|
|
specifier,
|
|
line_index.offset_tsc(params.text_document_position_params.position)?,
|
|
));
|
|
let maybe_implementations: Option<Vec<tsc::ImplementationLocation>> = self
|
|
.ts_server
|
|
.request(self.snapshot(), req)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Failed to request to tsserver {}", err);
|
|
LspError::invalid_request()
|
|
})?;
|
|
|
|
let result = if let Some(implementations) = maybe_implementations {
|
|
let mut links = Vec::new();
|
|
for implementation in implementations {
|
|
if let Some(link) = implementation.to_link(&line_index, self).await {
|
|
links.push(link)
|
|
}
|
|
}
|
|
Some(GotoDefinitionResponse::Link(links))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
self.performance.measure(mark);
|
|
Ok(result)
|
|
}
|
|
|
|
async fn folding_range(
|
|
&mut self,
|
|
params: FoldingRangeParams,
|
|
) -> LspResult<Option<Vec<FoldingRange>>> {
|
|
let specifier = self.url_map.normalize_url(¶ms.text_document.uri);
|
|
if !self.config.specifier_enabled(&specifier) {
|
|
return Ok(None);
|
|
}
|
|
let mark = self.performance.mark("folding_range", Some(¶ms));
|
|
|
|
let line_index =
|
|
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
|
line_index
|
|
} else {
|
|
return Err(LspError::invalid_params(format!(
|
|
"An unexpected specifier ({}) was provided.",
|
|
specifier
|
|
)));
|
|
};
|
|
|
|
let req = tsc::RequestMethod::GetOutliningSpans(specifier.clone());
|
|
let outlining_spans: Vec<tsc::OutliningSpan> = self
|
|
.ts_server
|
|
.request(self.snapshot(), req)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Failed to request to tsserver {}", err);
|
|
LspError::invalid_request()
|
|
})?;
|
|
|
|
let response = if !outlining_spans.is_empty() {
|
|
let text_content =
|
|
self.get_text_content(&specifier).ok_or_else(|| {
|
|
LspError::invalid_params(format!(
|
|
"An unexpected specifier ({}) was provided.",
|
|
specifier
|
|
))
|
|
})?;
|
|
Some(
|
|
outlining_spans
|
|
.iter()
|
|
.map(|span| {
|
|
span.to_folding_range(
|
|
&line_index,
|
|
text_content.as_str().as_bytes(),
|
|
self.config.client_capabilities.line_folding_only,
|
|
)
|
|
})
|
|
.collect::<Vec<FoldingRange>>(),
|
|
)
|
|
} else {
|
|
None
|
|
};
|
|
self.performance.measure(mark);
|
|
Ok(response)
|
|
}
|
|
|
|
async fn incoming_calls(
|
|
&mut self,
|
|
params: CallHierarchyIncomingCallsParams,
|
|
) -> LspResult<Option<Vec<CallHierarchyIncomingCall>>> {
|
|
let specifier = self.url_map.normalize_url(¶ms.item.uri);
|
|
if !self.config.specifier_enabled(&specifier) {
|
|
return Ok(None);
|
|
}
|
|
let mark = self.performance.mark("incoming_calls", Some(¶ms));
|
|
|
|
let line_index =
|
|
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
|
line_index
|
|
} else {
|
|
return Err(LspError::invalid_params(format!(
|
|
"An unexpected specifier ({}) was provided.",
|
|
specifier
|
|
)));
|
|
};
|
|
|
|
let req = tsc::RequestMethod::ProvideCallHierarchyIncomingCalls((
|
|
specifier.clone(),
|
|
line_index.offset_tsc(params.item.selection_range.start)?,
|
|
));
|
|
let incoming_calls: Vec<tsc::CallHierarchyIncomingCall> = self
|
|
.ts_server
|
|
.request(self.snapshot(), req)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Failed to request to tsserver {}", err);
|
|
LspError::invalid_request()
|
|
})?;
|
|
|
|
let maybe_root_path_owned = self
|
|
.config
|
|
.root_uri
|
|
.as_ref()
|
|
.and_then(|uri| uri.to_file_path().ok());
|
|
let mut resolved_items = Vec::<CallHierarchyIncomingCall>::new();
|
|
for item in incoming_calls.iter() {
|
|
if let Some(resolved) = item
|
|
.try_resolve_call_hierarchy_incoming_call(
|
|
self,
|
|
maybe_root_path_owned.as_deref(),
|
|
)
|
|
.await
|
|
{
|
|
resolved_items.push(resolved);
|
|
}
|
|
}
|
|
self.performance.measure(mark);
|
|
Ok(Some(resolved_items))
|
|
}
|
|
|
|
async fn outgoing_calls(
|
|
&mut self,
|
|
params: CallHierarchyOutgoingCallsParams,
|
|
) -> LspResult<Option<Vec<CallHierarchyOutgoingCall>>> {
|
|
let specifier = self.url_map.normalize_url(¶ms.item.uri);
|
|
if !self.config.specifier_enabled(&specifier) {
|
|
return Ok(None);
|
|
}
|
|
let mark = self.performance.mark("outgoing_calls", Some(¶ms));
|
|
|
|
let line_index =
|
|
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
|
line_index
|
|
} else {
|
|
return Err(LspError::invalid_params(format!(
|
|
"An unexpected specifier ({}) was provided.",
|
|
specifier
|
|
)));
|
|
};
|
|
|
|
let req = tsc::RequestMethod::ProvideCallHierarchyOutgoingCalls((
|
|
specifier.clone(),
|
|
line_index.offset_tsc(params.item.selection_range.start)?,
|
|
));
|
|
let outgoing_calls: Vec<tsc::CallHierarchyOutgoingCall> = self
|
|
.ts_server
|
|
.request(self.snapshot(), req)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Failed to request to tsserver {}", err);
|
|
LspError::invalid_request()
|
|
})?;
|
|
|
|
let maybe_root_path_owned = self
|
|
.config
|
|
.root_uri
|
|
.as_ref()
|
|
.and_then(|uri| uri.to_file_path().ok());
|
|
let mut resolved_items = Vec::<CallHierarchyOutgoingCall>::new();
|
|
for item in outgoing_calls.iter() {
|
|
if let Some(resolved) = item
|
|
.try_resolve_call_hierarchy_outgoing_call(
|
|
&line_index,
|
|
self,
|
|
maybe_root_path_owned.as_deref(),
|
|
)
|
|
.await
|
|
{
|
|
resolved_items.push(resolved);
|
|
}
|
|
}
|
|
self.performance.measure(mark);
|
|
Ok(Some(resolved_items))
|
|
}
|
|
|
|
async fn prepare_call_hierarchy(
|
|
&mut self,
|
|
params: CallHierarchyPrepareParams,
|
|
) -> LspResult<Option<Vec<CallHierarchyItem>>> {
|
|
let specifier = self
|
|
.url_map
|
|
.normalize_url(¶ms.text_document_position_params.text_document.uri);
|
|
if !self.config.specifier_enabled(&specifier) {
|
|
return Ok(None);
|
|
}
|
|
let mark = self
|
|
.performance
|
|
.mark("prepare_call_hierarchy", Some(¶ms));
|
|
|
|
let line_index =
|
|
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
|
line_index
|
|
} else {
|
|
return Err(LspError::invalid_params(format!(
|
|
"An unexpected specifier ({}) was provided.",
|
|
specifier
|
|
)));
|
|
};
|
|
|
|
let req = tsc::RequestMethod::PrepareCallHierarchy((
|
|
specifier.clone(),
|
|
line_index.offset_tsc(params.text_document_position_params.position)?,
|
|
));
|
|
let maybe_one_or_many: Option<tsc::OneOrMany<tsc::CallHierarchyItem>> =
|
|
self
|
|
.ts_server
|
|
.request(self.snapshot(), req)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Failed to request to tsserver {}", err);
|
|
LspError::invalid_request()
|
|
})?;
|
|
|
|
let response = if let Some(one_or_many) = maybe_one_or_many {
|
|
let maybe_root_path_owned = self
|
|
.config
|
|
.root_uri
|
|
.as_ref()
|
|
.and_then(|uri| uri.to_file_path().ok());
|
|
let mut resolved_items = Vec::<CallHierarchyItem>::new();
|
|
match one_or_many {
|
|
tsc::OneOrMany::One(item) => {
|
|
if let Some(resolved) = item
|
|
.try_resolve_call_hierarchy_item(
|
|
self,
|
|
maybe_root_path_owned.as_deref(),
|
|
)
|
|
.await
|
|
{
|
|
resolved_items.push(resolved)
|
|
}
|
|
}
|
|
tsc::OneOrMany::Many(items) => {
|
|
for item in items.iter() {
|
|
if let Some(resolved) = item
|
|
.try_resolve_call_hierarchy_item(
|
|
self,
|
|
maybe_root_path_owned.as_deref(),
|
|
)
|
|
.await
|
|
{
|
|
resolved_items.push(resolved);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Some(resolved_items)
|
|
} else {
|
|
None
|
|
};
|
|
self.performance.measure(mark);
|
|
Ok(response)
|
|
}
|
|
|
|
async fn rename(
|
|
&mut self,
|
|
params: RenameParams,
|
|
) -> LspResult<Option<WorkspaceEdit>> {
|
|
let specifier = self
|
|
.url_map
|
|
.normalize_url(¶ms.text_document_position.text_document.uri);
|
|
if !self.config.specifier_enabled(&specifier) {
|
|
return Ok(None);
|
|
}
|
|
let mark = self.performance.mark("rename", Some(¶ms));
|
|
|
|
let line_index =
|
|
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
|
line_index
|
|
} else {
|
|
return Err(LspError::invalid_params(format!(
|
|
"An unexpected specifier ({}) was provided.",
|
|
specifier
|
|
)));
|
|
};
|
|
|
|
let req = tsc::RequestMethod::FindRenameLocations((
|
|
specifier,
|
|
line_index.offset_tsc(params.text_document_position.position)?,
|
|
true,
|
|
true,
|
|
false,
|
|
));
|
|
|
|
let maybe_locations: Option<Vec<tsc::RenameLocation>> = self
|
|
.ts_server
|
|
.request(self.snapshot(), req)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Failed to request to tsserver {}", err);
|
|
LspError::invalid_request()
|
|
})?;
|
|
|
|
if let Some(locations) = maybe_locations {
|
|
let rename_locations = tsc::RenameLocations { locations };
|
|
let workspace_edits = rename_locations
|
|
.into_workspace_edit(¶ms.new_name, self)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Failed to get workspace edits: {}", err);
|
|
LspError::internal_error()
|
|
})?;
|
|
self.performance.measure(mark);
|
|
Ok(Some(workspace_edits))
|
|
} else {
|
|
self.performance.measure(mark);
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
async fn request_else(
|
|
&mut self,
|
|
method: &str,
|
|
params: Option<Value>,
|
|
) -> LspResult<Option<Value>> {
|
|
match method {
|
|
"deno/cache" => match params.map(serde_json::from_value) {
|
|
Some(Ok(params)) => self.cache(params).await,
|
|
Some(Err(err)) => Err(LspError::invalid_params(err.to_string())),
|
|
None => Err(LspError::invalid_params("Missing parameters")),
|
|
},
|
|
"deno/performance" => Ok(Some(self.get_performance())),
|
|
"deno/reloadImportRegistries" => self.reload_import_registries().await,
|
|
"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())
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn selection_range(
|
|
&mut self,
|
|
params: SelectionRangeParams,
|
|
) -> LspResult<Option<Vec<SelectionRange>>> {
|
|
let specifier = self.url_map.normalize_url(¶ms.text_document.uri);
|
|
if !self.config.specifier_enabled(&specifier) {
|
|
return Ok(None);
|
|
}
|
|
let mark = self.performance.mark("selection_range", Some(¶ms));
|
|
|
|
let line_index =
|
|
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
|
line_index
|
|
} else {
|
|
return Err(LspError::invalid_params(format!(
|
|
"An unexpected specifier ({}) was provided.",
|
|
specifier
|
|
)));
|
|
};
|
|
|
|
let mut selection_ranges = Vec::<SelectionRange>::new();
|
|
for position in params.positions {
|
|
let req = tsc::RequestMethod::GetSmartSelectionRange((
|
|
specifier.clone(),
|
|
line_index.offset_tsc(position)?,
|
|
));
|
|
|
|
let selection_range: tsc::SelectionRange = self
|
|
.ts_server
|
|
.request(self.snapshot(), req)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Failed to request to tsserver {}", err);
|
|
LspError::invalid_request()
|
|
})?;
|
|
|
|
selection_ranges.push(selection_range.to_selection_range(&line_index));
|
|
}
|
|
self.performance.measure(mark);
|
|
Ok(Some(selection_ranges))
|
|
}
|
|
|
|
async fn semantic_tokens_full(
|
|
&mut self,
|
|
params: SemanticTokensParams,
|
|
) -> LspResult<Option<SemanticTokensResult>> {
|
|
let specifier = self.url_map.normalize_url(¶ms.text_document.uri);
|
|
if !self.config.specifier_enabled(&specifier) {
|
|
return Ok(None);
|
|
}
|
|
let mark = self.performance.mark("semantic_tokens_full", Some(¶ms));
|
|
|
|
let line_index =
|
|
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
|
line_index
|
|
} else {
|
|
return Err(LspError::invalid_params(format!(
|
|
"An unexpected specifier ({}) was provided.",
|
|
specifier
|
|
)));
|
|
};
|
|
|
|
let req = tsc::RequestMethod::GetEncodedSemanticClassifications((
|
|
specifier.clone(),
|
|
tsc::TextSpan {
|
|
start: 0,
|
|
length: line_index.text_content_length_utf16().into(),
|
|
},
|
|
));
|
|
let semantic_classification: tsc::Classifications = self
|
|
.ts_server
|
|
.request(self.snapshot(), req)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Failed to request to tsserver {}", err);
|
|
LspError::invalid_request()
|
|
})?;
|
|
|
|
let semantic_tokens: SemanticTokens =
|
|
semantic_classification.to_semantic_tokens(&line_index);
|
|
let response = if !semantic_tokens.data.is_empty() {
|
|
Some(SemanticTokensResult::Tokens(semantic_tokens))
|
|
} else {
|
|
None
|
|
};
|
|
self.performance.measure(mark);
|
|
Ok(response)
|
|
}
|
|
|
|
async fn semantic_tokens_range(
|
|
&mut self,
|
|
params: SemanticTokensRangeParams,
|
|
) -> LspResult<Option<SemanticTokensRangeResult>> {
|
|
let specifier = self.url_map.normalize_url(¶ms.text_document.uri);
|
|
if !self.config.specifier_enabled(&specifier) {
|
|
return Ok(None);
|
|
}
|
|
let mark = self
|
|
.performance
|
|
.mark("semantic_tokens_range", Some(¶ms));
|
|
|
|
let line_index =
|
|
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
|
line_index
|
|
} else {
|
|
return Err(LspError::invalid_params(format!(
|
|
"An unexpected specifier ({}) was provided.",
|
|
specifier
|
|
)));
|
|
};
|
|
|
|
let start = line_index.offset_tsc(params.range.start)?;
|
|
let length = line_index.offset_tsc(params.range.end)? - start;
|
|
let req = tsc::RequestMethod::GetEncodedSemanticClassifications((
|
|
specifier.clone(),
|
|
tsc::TextSpan { start, length },
|
|
));
|
|
let semantic_classification: tsc::Classifications = self
|
|
.ts_server
|
|
.request(self.snapshot(), req)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Failed to request to tsserver {}", err);
|
|
LspError::invalid_request()
|
|
})?;
|
|
|
|
let semantic_tokens: SemanticTokens =
|
|
semantic_classification.to_semantic_tokens(&line_index);
|
|
let response = if !semantic_tokens.data.is_empty() {
|
|
Some(SemanticTokensRangeResult::Tokens(semantic_tokens))
|
|
} else {
|
|
None
|
|
};
|
|
self.performance.measure(mark);
|
|
Ok(response)
|
|
}
|
|
|
|
async fn signature_help(
|
|
&mut self,
|
|
params: SignatureHelpParams,
|
|
) -> LspResult<Option<SignatureHelp>> {
|
|
let specifier = self
|
|
.url_map
|
|
.normalize_url(¶ms.text_document_position_params.text_document.uri);
|
|
if !self.config.specifier_enabled(&specifier) {
|
|
return Ok(None);
|
|
}
|
|
let mark = self.performance.mark("signature_help", Some(¶ms));
|
|
let line_index =
|
|
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
|
line_index
|
|
} else {
|
|
return Err(LspError::invalid_params(format!(
|
|
"An unexpected specifier ({}) was provided.",
|
|
specifier
|
|
)));
|
|
};
|
|
let options = if let Some(context) = params.context {
|
|
tsc::SignatureHelpItemsOptions {
|
|
trigger_reason: Some(tsc::SignatureHelpTriggerReason {
|
|
kind: context.trigger_kind.into(),
|
|
trigger_character: context.trigger_character,
|
|
}),
|
|
}
|
|
} else {
|
|
tsc::SignatureHelpItemsOptions {
|
|
trigger_reason: None,
|
|
}
|
|
};
|
|
let req = tsc::RequestMethod::GetSignatureHelpItems((
|
|
specifier,
|
|
line_index.offset_tsc(params.text_document_position_params.position)?,
|
|
options,
|
|
));
|
|
let maybe_signature_help_items: Option<tsc::SignatureHelpItems> = self
|
|
.ts_server
|
|
.request(self.snapshot(), req)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Failed to request to tsserver: {}", err);
|
|
LspError::invalid_request()
|
|
})?;
|
|
|
|
if let Some(signature_help_items) = maybe_signature_help_items {
|
|
let signature_help = signature_help_items.into_signature_help();
|
|
self.performance.measure(mark);
|
|
Ok(Some(signature_help))
|
|
} else {
|
|
self.performance.measure(mark);
|
|
Ok(None)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[lspower::async_trait]
|
|
impl lspower::LanguageServer for LanguageServer {
|
|
async fn initialize(
|
|
&self,
|
|
params: InitializeParams,
|
|
) -> LspResult<InitializeResult> {
|
|
let mut language_server = self.0.lock().await;
|
|
let client = language_server.client.clone();
|
|
let ts_server = language_server.ts_server.clone();
|
|
language_server
|
|
.diagnostics_server
|
|
.start(self.0.clone(), client, ts_server);
|
|
language_server.initialize(params).await
|
|
}
|
|
|
|
async fn initialized(&self, params: InitializedParams) {
|
|
self.0.lock().await.initialized(params).await
|
|
}
|
|
|
|
async fn shutdown(&self) -> LspResult<()> {
|
|
self.0.lock().await.shutdown().await
|
|
}
|
|
|
|
async fn did_open(&self, params: DidOpenTextDocumentParams) {
|
|
self.0.lock().await.did_open(params).await
|
|
}
|
|
|
|
async fn did_change(&self, params: DidChangeTextDocumentParams) {
|
|
self.0.lock().await.did_change(params).await
|
|
}
|
|
|
|
async fn did_save(&self, _params: DidSaveTextDocumentParams) {
|
|
// We don't need to do anything on save at the moment, but if this isn't
|
|
// implemented, lspower complains about it not being implemented.
|
|
}
|
|
|
|
async fn did_close(&self, params: DidCloseTextDocumentParams) {
|
|
self.0.lock().await.did_close(params).await
|
|
}
|
|
|
|
async fn did_change_configuration(
|
|
&self,
|
|
params: DidChangeConfigurationParams,
|
|
) {
|
|
self.0.lock().await.did_change_configuration(params).await
|
|
}
|
|
|
|
async fn did_change_watched_files(
|
|
&self,
|
|
params: DidChangeWatchedFilesParams,
|
|
) {
|
|
self.0.lock().await.did_change_watched_files(params).await
|
|
}
|
|
|
|
async fn document_symbol(
|
|
&self,
|
|
params: DocumentSymbolParams,
|
|
) -> LspResult<Option<DocumentSymbolResponse>> {
|
|
self.0.lock().await.document_symbol(params).await
|
|
}
|
|
|
|
async fn formatting(
|
|
&self,
|
|
params: DocumentFormattingParams,
|
|
) -> LspResult<Option<Vec<TextEdit>>> {
|
|
self.0.lock().await.formatting(params).await
|
|
}
|
|
|
|
async fn hover(&self, params: HoverParams) -> LspResult<Option<Hover>> {
|
|
self.0.lock().await.hover(params).await
|
|
}
|
|
|
|
async fn code_action(
|
|
&self,
|
|
params: CodeActionParams,
|
|
) -> LspResult<Option<CodeActionResponse>> {
|
|
self.0.lock().await.code_action(params).await
|
|
}
|
|
|
|
async fn code_action_resolve(
|
|
&self,
|
|
params: CodeAction,
|
|
) -> LspResult<CodeAction> {
|
|
self.0.lock().await.code_action_resolve(params).await
|
|
}
|
|
|
|
async fn code_lens(
|
|
&self,
|
|
params: CodeLensParams,
|
|
) -> LspResult<Option<Vec<CodeLens>>> {
|
|
self.0.lock().await.code_lens(params).await
|
|
}
|
|
|
|
async fn code_lens_resolve(&self, params: CodeLens) -> LspResult<CodeLens> {
|
|
self.0.lock().await.code_lens_resolve(params).await
|
|
}
|
|
|
|
async fn document_highlight(
|
|
&self,
|
|
params: DocumentHighlightParams,
|
|
) -> LspResult<Option<Vec<DocumentHighlight>>> {
|
|
self.0.lock().await.document_highlight(params).await
|
|
}
|
|
|
|
async fn references(
|
|
&self,
|
|
params: ReferenceParams,
|
|
) -> LspResult<Option<Vec<Location>>> {
|
|
self.0.lock().await.references(params).await
|
|
}
|
|
|
|
async fn goto_definition(
|
|
&self,
|
|
params: GotoDefinitionParams,
|
|
) -> LspResult<Option<GotoDefinitionResponse>> {
|
|
self.0.lock().await.goto_definition(params).await
|
|
}
|
|
|
|
async fn completion(
|
|
&self,
|
|
params: CompletionParams,
|
|
) -> LspResult<Option<CompletionResponse>> {
|
|
self.0.lock().await.completion(params).await
|
|
}
|
|
|
|
async fn completion_resolve(
|
|
&self,
|
|
params: CompletionItem,
|
|
) -> LspResult<CompletionItem> {
|
|
self.0.lock().await.completion_resolve(params).await
|
|
}
|
|
|
|
async fn goto_implementation(
|
|
&self,
|
|
params: GotoImplementationParams,
|
|
) -> LspResult<Option<GotoImplementationResponse>> {
|
|
self.0.lock().await.goto_implementation(params).await
|
|
}
|
|
|
|
async fn folding_range(
|
|
&self,
|
|
params: FoldingRangeParams,
|
|
) -> LspResult<Option<Vec<FoldingRange>>> {
|
|
self.0.lock().await.folding_range(params).await
|
|
}
|
|
|
|
async fn incoming_calls(
|
|
&self,
|
|
params: CallHierarchyIncomingCallsParams,
|
|
) -> LspResult<Option<Vec<CallHierarchyIncomingCall>>> {
|
|
self.0.lock().await.incoming_calls(params).await
|
|
}
|
|
|
|
async fn outgoing_calls(
|
|
&self,
|
|
params: CallHierarchyOutgoingCallsParams,
|
|
) -> LspResult<Option<Vec<CallHierarchyOutgoingCall>>> {
|
|
self.0.lock().await.outgoing_calls(params).await
|
|
}
|
|
|
|
async fn prepare_call_hierarchy(
|
|
&self,
|
|
params: CallHierarchyPrepareParams,
|
|
) -> LspResult<Option<Vec<CallHierarchyItem>>> {
|
|
self.0.lock().await.prepare_call_hierarchy(params).await
|
|
}
|
|
|
|
async fn rename(
|
|
&self,
|
|
params: RenameParams,
|
|
) -> LspResult<Option<WorkspaceEdit>> {
|
|
self.0.lock().await.rename(params).await
|
|
}
|
|
|
|
async fn request_else(
|
|
&self,
|
|
method: &str,
|
|
params: Option<Value>,
|
|
) -> LspResult<Option<Value>> {
|
|
self.0.lock().await.request_else(method, params).await
|
|
}
|
|
|
|
async fn selection_range(
|
|
&self,
|
|
params: SelectionRangeParams,
|
|
) -> LspResult<Option<Vec<SelectionRange>>> {
|
|
self.0.lock().await.selection_range(params).await
|
|
}
|
|
|
|
async fn semantic_tokens_full(
|
|
&self,
|
|
params: SemanticTokensParams,
|
|
) -> LspResult<Option<SemanticTokensResult>> {
|
|
self.0.lock().await.semantic_tokens_full(params).await
|
|
}
|
|
|
|
async fn semantic_tokens_range(
|
|
&self,
|
|
params: SemanticTokensRangeParams,
|
|
) -> LspResult<Option<SemanticTokensRangeResult>> {
|
|
self.0.lock().await.semantic_tokens_range(params).await
|
|
}
|
|
|
|
async fn signature_help(
|
|
&self,
|
|
params: SignatureHelpParams,
|
|
) -> LspResult<Option<SignatureHelp>> {
|
|
self.0.lock().await.signature_help(params).await
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct CacheParams {
|
|
/// The document currently open in the editor. If there are no `uris`
|
|
/// supplied, the referrer will be cached.
|
|
referrer: TextDocumentIdentifier,
|
|
/// Any documents that have been specifically asked to be cached via the
|
|
/// command.
|
|
uris: Vec<TextDocumentIdentifier>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct VirtualTextDocumentParams {
|
|
text_document: TextDocumentIdentifier,
|
|
}
|
|
|
|
// These are implementations of custom commands supported by the LSP
|
|
impl Inner {
|
|
/// Similar to `deno cache` on the command line, where modules will be cached
|
|
/// in the Deno cache, including any of their dependencies.
|
|
async fn cache(&mut self, params: CacheParams) -> LspResult<Option<Value>> {
|
|
let mark = self.performance.mark("cache", Some(¶ms));
|
|
let referrer = self.url_map.normalize_url(¶ms.referrer.uri);
|
|
if !params.uris.is_empty() {
|
|
for identifier in ¶ms.uris {
|
|
let specifier = self.url_map.normalize_url(&identifier.uri);
|
|
sources::cache(&specifier, &self.maybe_import_map)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("{}", err);
|
|
LspError::internal_error()
|
|
})?;
|
|
}
|
|
} else {
|
|
sources::cache(&referrer, &self.maybe_import_map)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("{}", err);
|
|
LspError::internal_error()
|
|
})?;
|
|
}
|
|
// now that we have dependencies loaded, we need to re-analyze them and
|
|
// invalidate some diagnostics
|
|
if self.documents.contains_key(&referrer) {
|
|
if let Some(source) = self.documents.content(&referrer).unwrap() {
|
|
self.analyze_dependencies(&referrer, &source);
|
|
}
|
|
self.diagnostics_server.invalidate(&referrer).await;
|
|
}
|
|
|
|
self.diagnostics_server.update().map_err(|err| {
|
|
error!("{}", err);
|
|
LspError::internal_error()
|
|
})?;
|
|
self.performance.measure(mark);
|
|
Ok(Some(json!(true)))
|
|
}
|
|
|
|
fn get_performance(&self) -> Value {
|
|
let averages = self.performance.averages();
|
|
json!({ "averages": averages })
|
|
}
|
|
|
|
async fn reload_import_registries(&mut self) -> LspResult<Option<Value>> {
|
|
fs::remove_dir_all(&self.module_registries_location)
|
|
.await
|
|
.map_err(|err| {
|
|
error!("Unable to remove registries cache: {}", err);
|
|
LspError::internal_error()
|
|
})?;
|
|
self.module_registries =
|
|
registries::ModuleRegistry::new(&self.module_registries_location);
|
|
self.update_registries().await.map_err(|err| {
|
|
error!("Unable to update registries: {}", err);
|
|
LspError::internal_error()
|
|
})?;
|
|
Ok(Some(json!(true)))
|
|
}
|
|
|
|
async fn virtual_text_document(
|
|
&mut self,
|
|
params: VirtualTextDocumentParams,
|
|
) -> LspResult<Option<String>> {
|
|
let mark = self
|
|
.performance
|
|
.mark("virtual_text_document", Some(¶ms));
|
|
let specifier = self.url_map.normalize_url(¶ms.text_document.uri);
|
|
let contents = if specifier.as_str() == "deno:/status.md" {
|
|
let mut contents = String::new();
|
|
let mut documents_specifiers = self.documents.specifiers();
|
|
documents_specifiers.sort();
|
|
let mut sources_specifiers = self.sources.specifiers();
|
|
sources_specifiers.sort();
|
|
let measures = self.performance.to_vec();
|
|
|
|
contents.push_str(&format!(
|
|
r#"# Deno Language Server Status
|
|
|
|
- <details><summary>Documents in memory: {}</summary>
|
|
|
|
- {}
|
|
|
|
</details>
|
|
|
|
- <details><summary>Sources in memory: {}</summary>
|
|
|
|
- {}
|
|
|
|
</details>
|
|
|
|
- <details><summary>Performance measures: {}</summary>
|
|
|
|
- {}
|
|
|
|
</details>
|
|
"#,
|
|
self.documents.len(),
|
|
documents_specifiers
|
|
.into_iter()
|
|
.map(|s| s.to_string())
|
|
.collect::<Vec<String>>()
|
|
.join("\n - "),
|
|
self.sources.len(),
|
|
sources_specifiers
|
|
.into_iter()
|
|
.map(|s| s.to_string())
|
|
.collect::<Vec<String>>()
|
|
.join("\n - "),
|
|
measures.len(),
|
|
measures
|
|
.iter()
|
|
.map(|m| m.to_string())
|
|
.collect::<Vec<String>>()
|
|
.join("\n - ")
|
|
));
|
|
contents
|
|
.push_str("\n## Performance\n\n|Name|Duration|Count|\n|---|---|---|\n");
|
|
let mut averages = self.performance.averages();
|
|
averages.sort();
|
|
for average in averages {
|
|
contents.push_str(&format!(
|
|
"|{}|{}ms|{}|\n",
|
|
average.name, average.average_duration, average.count
|
|
));
|
|
}
|
|
Some(contents)
|
|
} else {
|
|
match specifier.scheme() {
|
|
"asset" => {
|
|
if let Some(asset) = self
|
|
.get_asset(&specifier)
|
|
.await
|
|
.map_err(|_| LspError::internal_error())?
|
|
{
|
|
Some(asset.text)
|
|
} else {
|
|
error!("Missing asset: {}", specifier);
|
|
None
|
|
}
|
|
}
|
|
_ => {
|
|
if let Some(source) = self.sources.get_source(&specifier) {
|
|
Some(source)
|
|
} else {
|
|
error!("The cached source was not found: {}", specifier);
|
|
None
|
|
}
|
|
}
|
|
}
|
|
};
|
|
self.performance.measure(mark);
|
|
Ok(contents)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::lsp::performance::PerformanceAverage;
|
|
use lspower::jsonrpc;
|
|
use lspower::ExitedError;
|
|
use lspower::LspService;
|
|
use std::fs;
|
|
use std::task::Poll;
|
|
use std::time::Instant;
|
|
use tempfile::TempDir;
|
|
use tower_test::mock::Spawn;
|
|
|
|
enum LspResponse<V>
|
|
where
|
|
V: FnOnce(Value),
|
|
{
|
|
None,
|
|
Delay(u64),
|
|
RequestAny,
|
|
Request(u64, Value),
|
|
RequestAssert(V),
|
|
RequestFixture(u64, String),
|
|
}
|
|
|
|
enum LspFixture {
|
|
None,
|
|
Path(&'static str),
|
|
Value(Value),
|
|
}
|
|
|
|
type LspTestHarnessRequest = (LspFixture, LspResponse<fn(Value)>);
|
|
|
|
struct LspTestHarness {
|
|
requests: Vec<LspTestHarnessRequest>,
|
|
service: Spawn<LspService>,
|
|
}
|
|
|
|
impl LspTestHarness {
|
|
pub fn new(requests: Vec<LspTestHarnessRequest>) -> Self {
|
|
let (service, _) = LspService::new(LanguageServer::new);
|
|
let service = Spawn::new(service);
|
|
Self { requests, service }
|
|
}
|
|
|
|
async fn run(&mut self) {
|
|
for (value_or_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 response: Result<Option<jsonrpc::Outgoing>, ExitedError> =
|
|
match value_or_str {
|
|
LspFixture::None => Ok(None),
|
|
LspFixture::Path(req_path_str) => {
|
|
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();
|
|
self.service.call(req).await
|
|
}
|
|
LspFixture::Value(value) => {
|
|
let req: jsonrpc::Incoming =
|
|
serde_json::from_value(value.clone()).unwrap();
|
|
self.service.call(req).await
|
|
}
|
|
};
|
|
match response {
|
|
Ok(result) => match expected {
|
|
LspResponse::None => assert_eq!(result, None),
|
|
LspResponse::Delay(millis) => {
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(*millis))
|
|
.await
|
|
}
|
|
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),
|
|
},
|
|
LspResponse::RequestAssert(assert) => match result {
|
|
Some(jsonrpc::Outgoing::Response(resp)) => assert(json!(resp)),
|
|
_ => panic!("unexpected result: {:?}", result),
|
|
},
|
|
LspResponse::RequestFixture(id, res_path_str) => {
|
|
let res_path = fixtures_path.join(res_path_str);
|
|
let res_str = fs::read_to_string(res_path).unwrap();
|
|
match result {
|
|
Some(jsonrpc::Outgoing::Response(resp)) => assert_eq!(
|
|
resp,
|
|
jsonrpc::Response::ok(
|
|
jsonrpc::Id::Number(*id),
|
|
serde_json::from_str(&res_str).unwrap()
|
|
)
|
|
),
|
|
_ => panic!("unexpected result: {:?}", result),
|
|
}
|
|
}
|
|
},
|
|
Err(err) => panic!("Error result: {}", err),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_startup_shutdown() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_hover() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_open_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("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
|
|
}
|
|
}
|
|
}),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_hover_asset() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_open_notification_asset.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("definition_request_asset.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("virtual_text_document_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("hover_request_asset.json"),
|
|
LspResponse::Request(
|
|
5,
|
|
json!({
|
|
"contents": [
|
|
{
|
|
"language": "typescript",
|
|
"value": "interface Date",
|
|
},
|
|
"Enables basic storage and retrieval of dates and times."
|
|
],
|
|
"range": {
|
|
"start": {
|
|
"line": 109,
|
|
"character": 10,
|
|
},
|
|
"end": {
|
|
"line": 109,
|
|
"character": 14,
|
|
}
|
|
}
|
|
}),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_hover_disabled() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request_disabled.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_open_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("hover_request.json"),
|
|
LspResponse::Request(2, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_hover_unstable_disabled() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_open_notification_unstable.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("hover_request.json"),
|
|
LspResponse::Request(
|
|
2,
|
|
json!({
|
|
"contents": [
|
|
{
|
|
"language": "typescript",
|
|
"value": "any"
|
|
}
|
|
],
|
|
"range": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 17
|
|
},
|
|
"end": {
|
|
"line": 0,
|
|
"character": 27
|
|
}
|
|
}
|
|
}),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_hover_unstable_enabled() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request_unstable.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_open_notification_unstable.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("hover_request.json"),
|
|
LspResponse::Request(
|
|
2,
|
|
json!({
|
|
"contents": [
|
|
{
|
|
"language": "typescript",
|
|
"value": "function Deno.openPlugin(filename: string): number"
|
|
},
|
|
"**UNSTABLE**: new API, yet to be vetted.\n\nOpen and initialize a plugin.\n\n```ts\nconst rid = Deno.openPlugin(\"./path/to/some/plugin.so\");\nconst opId = Deno.core.ops()[\"some_op\"];\nconst response = Deno.core.dispatch(opId, new Uint8Array([1,2,3,4]));\nconsole.log(`Response from plugin ${response}`);\n```\n\nRequires `allow-plugin` permission.\n\nThe plugin system is not stable and will change in the future, hence the\nlack of docs. For now take a look at the example\nhttps://github.com/denoland/deno/tree/master/test_plugin"
|
|
],
|
|
"range": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 17
|
|
},
|
|
"end": {
|
|
"line": 0,
|
|
"character": 27
|
|
}
|
|
}
|
|
}),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_hover_change_mbc() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_open_notification_mbc.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_change_notification_mbc.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("hover_request_mbc.json"),
|
|
LspResponse::Request(
|
|
2,
|
|
json!({
|
|
"contents": [
|
|
{
|
|
"language": "typescript",
|
|
"value": "const b: \"🦕😃\"",
|
|
},
|
|
"",
|
|
],
|
|
"range": {
|
|
"start": {
|
|
"line": 2,
|
|
"character": 15,
|
|
},
|
|
"end": {
|
|
"line": 2,
|
|
"character": 16,
|
|
},
|
|
}
|
|
}),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct HoverResponse {
|
|
pub result: Option<Hover>,
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_hover_closed_document() {
|
|
let temp_dir = TempDir::new()
|
|
.expect("could not create temp dir")
|
|
.into_path();
|
|
let a_path = temp_dir.join("a.ts");
|
|
fs::write(a_path, r#"export const a = "a";"#)
|
|
.expect("could not write file");
|
|
let b_path = temp_dir.join("b.ts");
|
|
fs::write(&b_path, r#"export * from "./a.ts";"#)
|
|
.expect("could not write file");
|
|
let b_specifier =
|
|
Url::from_file_path(b_path).expect("could not convert path");
|
|
let c_path = temp_dir.join("c.ts");
|
|
fs::write(&c_path, "import { a } from \"./b.ts\";\nconsole.log(a);\n")
|
|
.expect("could not write file");
|
|
let c_specifier =
|
|
Url::from_file_path(c_path).expect("could not convert path");
|
|
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Value(json!({
|
|
"jsonrpc": "2.0",
|
|
"method": "textDocument/didOpen",
|
|
"params": {
|
|
"textDocument": {
|
|
"uri": b_specifier,
|
|
"languageId": "typescript",
|
|
"version": 1,
|
|
"text": r#"export * from "./a.ts";"#
|
|
}
|
|
}
|
|
})),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Value(json!({
|
|
"jsonrpc": "2.0",
|
|
"method": "textDocument/didOpen",
|
|
"params": {
|
|
"textDocument": {
|
|
"uri": c_specifier,
|
|
"languageId": "typescript",
|
|
"version": 1,
|
|
"text": "import { a } from \"./b.ts\";\nconsole.log(a);\n",
|
|
}
|
|
}
|
|
})),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Value(json!({
|
|
"jsonrpc": "2.0",
|
|
"id": 2,
|
|
"method": "textDocument/hover",
|
|
"params": {
|
|
"textDocument": {
|
|
"uri": c_specifier,
|
|
},
|
|
"position": {
|
|
"line": 0,
|
|
"character": 10
|
|
}
|
|
}
|
|
})),
|
|
LspResponse::RequestAssert(|value| {
|
|
let resp: HoverResponse = serde_json::from_value(value).unwrap();
|
|
if let Some(hover) = resp.result {
|
|
assert_eq!(
|
|
hover,
|
|
Hover {
|
|
contents: HoverContents::Array(vec![
|
|
MarkedString::LanguageString(LanguageString {
|
|
language: "typescript".to_string(),
|
|
value: "(alias) const a: \"a\"\nimport a".to_string()
|
|
}),
|
|
MarkedString::String("".to_string()),
|
|
]),
|
|
range: Some(Range {
|
|
start: Position {
|
|
line: 0,
|
|
character: 9,
|
|
},
|
|
end: Position {
|
|
line: 0,
|
|
character: 10,
|
|
}
|
|
}),
|
|
}
|
|
);
|
|
} else {
|
|
panic!("no response");
|
|
}
|
|
}),
|
|
),
|
|
(
|
|
LspFixture::Value(json!({
|
|
"jsonrpc": "2.0",
|
|
"method": "textDocument/didClose",
|
|
"params": {
|
|
"textDocument": {
|
|
"uri": b_specifier,
|
|
}
|
|
}
|
|
})),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Value(json!({
|
|
"jsonrpc": "2.0",
|
|
"id": 4,
|
|
"method": "textDocument/hover",
|
|
"params": {
|
|
"textDocument": {
|
|
"uri": c_specifier,
|
|
},
|
|
"position": {
|
|
"line": 0,
|
|
"character": 10
|
|
}
|
|
}
|
|
})),
|
|
LspResponse::RequestAssert(|value| {
|
|
let resp: HoverResponse = serde_json::from_value(value).unwrap();
|
|
if let Some(hover) = resp.result {
|
|
assert_eq!(
|
|
hover,
|
|
Hover {
|
|
contents: HoverContents::Array(vec![
|
|
MarkedString::LanguageString(LanguageString {
|
|
language: "typescript".to_string(),
|
|
value: "(alias) const a: \"a\"\nimport a".to_string()
|
|
}),
|
|
MarkedString::String("".to_string()),
|
|
]),
|
|
range: Some(Range {
|
|
start: Position {
|
|
line: 0,
|
|
character: 9,
|
|
},
|
|
end: Position {
|
|
line: 0,
|
|
character: 10,
|
|
}
|
|
}),
|
|
}
|
|
);
|
|
} else {
|
|
panic!("no response");
|
|
}
|
|
}),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_call_hierarchy() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("prepare_call_hierarchy_did_open_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("prepare_call_hierarchy_request.json"),
|
|
LspResponse::Request(
|
|
2,
|
|
json!([
|
|
{
|
|
"name": "baz",
|
|
"kind": 6,
|
|
"detail": "Bar",
|
|
"uri": "file:///a/file.ts",
|
|
"range": {
|
|
"start": {
|
|
"line": 5,
|
|
"character": 2
|
|
},
|
|
"end": {
|
|
"line": 7,
|
|
"character": 3
|
|
}
|
|
},
|
|
"selectionRange": {
|
|
"start": {
|
|
"line": 5,
|
|
"character": 2
|
|
},
|
|
"end": {
|
|
"line": 5,
|
|
"character": 5
|
|
}
|
|
}
|
|
}
|
|
]),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("incoming_calls_request.json"),
|
|
LspResponse::Request(
|
|
4,
|
|
json!([
|
|
{
|
|
"from": {
|
|
"name": "main",
|
|
"kind": 12,
|
|
"detail": "",
|
|
"uri": "file:///a/file.ts",
|
|
"range": {
|
|
"start": {
|
|
"line": 10,
|
|
"character": 0
|
|
},
|
|
"end": {
|
|
"line": 13,
|
|
"character": 1
|
|
}
|
|
},
|
|
"selectionRange": {
|
|
"start": {
|
|
"line": 10,
|
|
"character": 9
|
|
},
|
|
"end": {
|
|
"line": 10,
|
|
"character": 13
|
|
}
|
|
}
|
|
},
|
|
"fromRanges": [
|
|
{
|
|
"start": {
|
|
"line": 12,
|
|
"character": 6
|
|
},
|
|
"end": {
|
|
"line": 12,
|
|
"character": 9
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("outgoing_calls_request.json"),
|
|
LspResponse::Request(
|
|
5,
|
|
json!([
|
|
{
|
|
"to": {
|
|
"name": "foo",
|
|
"kind": 12,
|
|
"detail": "",
|
|
"uri": "file:///a/file.ts",
|
|
"range": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 0
|
|
},
|
|
"end": {
|
|
"line": 2,
|
|
"character": 1
|
|
}
|
|
},
|
|
"selectionRange": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 9
|
|
},
|
|
"end": {
|
|
"line": 0,
|
|
"character": 12
|
|
}
|
|
}
|
|
},
|
|
"fromRanges": [
|
|
{
|
|
"start": {
|
|
"line": 6,
|
|
"character": 11
|
|
},
|
|
"end": {
|
|
"line": 6,
|
|
"character": 14
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_format_mbc() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_open_notification_mbc_fmt.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("formatting_request_mbc_fmt.json"),
|
|
LspResponse::Request(
|
|
2,
|
|
json!([
|
|
{
|
|
"range": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 12
|
|
},
|
|
"end": {
|
|
"line": 0,
|
|
"character": 13,
|
|
}
|
|
},
|
|
"newText": "\""
|
|
},
|
|
{
|
|
"range": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 21
|
|
},
|
|
"end": {
|
|
"line": 0,
|
|
"character": 22
|
|
}
|
|
},
|
|
"newText": "\";"
|
|
},
|
|
{
|
|
"range": {
|
|
"start": {
|
|
"line": 1,
|
|
"character": 12,
|
|
},
|
|
"end": {
|
|
"line": 1,
|
|
"character": 13,
|
|
}
|
|
},
|
|
"newText": "\""
|
|
},
|
|
{
|
|
"range": {
|
|
"start": {
|
|
"line": 1,
|
|
"character": 23,
|
|
},
|
|
"end": {
|
|
"line": 1,
|
|
"character": 25,
|
|
}
|
|
},
|
|
"newText": "\");"
|
|
}
|
|
]),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore] // TODO(ry) Re-enable. Flaky on ubuntu-latest-xl.
|
|
async fn test_large_doc_change() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_open_notification_large.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_change_notification_large.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_change_notification_large_02.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_change_notification_large_03.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("hover_request_large_01.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("hover_request_large_02.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("hover_request_large_03.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
let time = Instant::now();
|
|
harness.run().await;
|
|
assert!(
|
|
time.elapsed().as_millis() <= 10000,
|
|
"the execution time exceeded 10000ms"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_document_symbol() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("document_symbol_did_open_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("document_symbol_request.json"),
|
|
LspResponse::Request(
|
|
2,
|
|
json!([
|
|
{
|
|
"name": "bar",
|
|
"kind": 13,
|
|
"range": {
|
|
"start": {
|
|
"line": 17,
|
|
"character": 4
|
|
},
|
|
"end": {
|
|
"line": 17,
|
|
"character": 26
|
|
}
|
|
},
|
|
"selectionRange": {
|
|
"start": {
|
|
"line": 17,
|
|
"character": 4
|
|
},
|
|
"end": {
|
|
"line": 17,
|
|
"character": 7
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "Bar",
|
|
"kind": 5,
|
|
"range": {
|
|
"start": {
|
|
"line": 4,
|
|
"character": 0
|
|
},
|
|
"end": {
|
|
"line": 13,
|
|
"character": 1
|
|
}
|
|
},
|
|
"selectionRange": {
|
|
"start": {
|
|
"line": 4,
|
|
"character": 6
|
|
},
|
|
"end": {
|
|
"line": 4,
|
|
"character": 9
|
|
}
|
|
},
|
|
"children": [
|
|
{
|
|
"name": "constructor",
|
|
"kind": 9,
|
|
"range": {
|
|
"start": {
|
|
"line": 5,
|
|
"character": 2
|
|
},
|
|
"end": {
|
|
"line": 5,
|
|
"character": 35
|
|
}
|
|
},
|
|
"selectionRange": {
|
|
"start": {
|
|
"line": 5,
|
|
"character": 2
|
|
},
|
|
"end": {
|
|
"line": 5,
|
|
"character": 35
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "baz",
|
|
"kind": 6,
|
|
"tags": [
|
|
1
|
|
],
|
|
"range": {
|
|
"start": {
|
|
"line": 8,
|
|
"character": 2
|
|
},
|
|
"end": {
|
|
"line": 8,
|
|
"character": 25
|
|
}
|
|
},
|
|
"selectionRange": {
|
|
"start": {
|
|
"line": 8,
|
|
"character": 2
|
|
},
|
|
"end": {
|
|
"line": 8,
|
|
"character": 5
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "foo",
|
|
"kind": 6,
|
|
"range": {
|
|
"start": {
|
|
"line": 6,
|
|
"character": 2
|
|
},
|
|
"end": {
|
|
"line": 6,
|
|
"character": 24
|
|
}
|
|
},
|
|
"selectionRange": {
|
|
"start": {
|
|
"line": 6,
|
|
"character": 2
|
|
},
|
|
"end": {
|
|
"line": 6,
|
|
"character": 5
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "getStaticBar",
|
|
"kind": 6,
|
|
"range": {
|
|
"start": {
|
|
"line": 12,
|
|
"character": 2
|
|
},
|
|
"end": {
|
|
"line": 12,
|
|
"character": 57
|
|
}
|
|
},
|
|
"selectionRange": {
|
|
"start": {
|
|
"line": 12,
|
|
"character": 17
|
|
},
|
|
"end": {
|
|
"line": 12,
|
|
"character": 29
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "staticBar",
|
|
"kind": 7,
|
|
"range": {
|
|
"start": {
|
|
"line": 11,
|
|
"character": 2
|
|
},
|
|
"end": {
|
|
"line": 11,
|
|
"character": 32
|
|
}
|
|
},
|
|
"selectionRange": {
|
|
"start": {
|
|
"line": 11,
|
|
"character": 9
|
|
},
|
|
"end": {
|
|
"line": 11,
|
|
"character": 18
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "value",
|
|
"kind": 7,
|
|
"range": {
|
|
"start": {
|
|
"line": 9,
|
|
"character": 2
|
|
},
|
|
"end": {
|
|
"line": 9,
|
|
"character": 35
|
|
}
|
|
},
|
|
"selectionRange": {
|
|
"start": {
|
|
"line": 9,
|
|
"character": 6
|
|
},
|
|
"end": {
|
|
"line": 9,
|
|
"character": 11
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "value",
|
|
"kind": 7,
|
|
"range": {
|
|
"start": {
|
|
"line": 10,
|
|
"character": 2
|
|
},
|
|
"end": {
|
|
"line": 10,
|
|
"character": 42
|
|
}
|
|
},
|
|
"selectionRange": {
|
|
"start": {
|
|
"line": 10,
|
|
"character": 6
|
|
},
|
|
"end": {
|
|
"line": 10,
|
|
"character": 11
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "x",
|
|
"kind": 7,
|
|
"range": {
|
|
"start": {
|
|
"line": 5,
|
|
"character": 14
|
|
},
|
|
"end": {
|
|
"line": 5,
|
|
"character": 30
|
|
}
|
|
},
|
|
"selectionRange": {
|
|
"start": {
|
|
"line": 5,
|
|
"character": 21
|
|
},
|
|
"end": {
|
|
"line": 5,
|
|
"character": 22
|
|
}
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "IFoo",
|
|
"kind": 11,
|
|
"range": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 0
|
|
},
|
|
"end": {
|
|
"line": 2,
|
|
"character": 1
|
|
}
|
|
},
|
|
"selectionRange": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 10
|
|
},
|
|
"end": {
|
|
"line": 0,
|
|
"character": 14
|
|
}
|
|
},
|
|
"children": [
|
|
{
|
|
"name": "foo",
|
|
"kind": 6,
|
|
"range": {
|
|
"start": {
|
|
"line": 1,
|
|
"character": 2
|
|
},
|
|
"end": {
|
|
"line": 1,
|
|
"character": 17
|
|
}
|
|
},
|
|
"selectionRange": {
|
|
"start": {
|
|
"line": 1,
|
|
"character": 2
|
|
},
|
|
"end": {
|
|
"line": 1,
|
|
"character": 5
|
|
}
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Values",
|
|
"kind": 10,
|
|
"range": {
|
|
"start": {
|
|
"line": 15,
|
|
"character": 0
|
|
},
|
|
"end": {
|
|
"line": 15,
|
|
"character": 30
|
|
}
|
|
},
|
|
"selectionRange": {
|
|
"start": {
|
|
"line": 15,
|
|
"character": 5
|
|
},
|
|
"end": {
|
|
"line": 15,
|
|
"character": 11
|
|
}
|
|
},
|
|
"children": [
|
|
{
|
|
"name": "value1",
|
|
"kind": 13,
|
|
"range": {
|
|
"start": {
|
|
"line": 15,
|
|
"character": 14
|
|
},
|
|
"end": {
|
|
"line": 15,
|
|
"character": 20
|
|
}
|
|
},
|
|
"selectionRange": {
|
|
"start": {
|
|
"line": 15,
|
|
"character": 14
|
|
},
|
|
"end": {
|
|
"line": 15,
|
|
"character": 20
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "value2",
|
|
"kind": 13,
|
|
"range": {
|
|
"start": {
|
|
"line": 15,
|
|
"character": 22
|
|
},
|
|
"end": {
|
|
"line": 15,
|
|
"character": 28
|
|
}
|
|
},
|
|
"selectionRange": {
|
|
"start": {
|
|
"line": 15,
|
|
"character": 22
|
|
},
|
|
"end": {
|
|
"line": 15,
|
|
"character": 28
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_folding_range() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("folding_range_did_open_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("folding_range_request.json"),
|
|
LspResponse::Request(
|
|
2,
|
|
json!([
|
|
{
|
|
"startLine": 0,
|
|
"endLine": 12,
|
|
"kind": "region"
|
|
},
|
|
{
|
|
"startLine": 1,
|
|
"endLine": 3,
|
|
"kind": "comment"
|
|
},
|
|
{
|
|
"startLine": 4,
|
|
"endLine": 10
|
|
},
|
|
{
|
|
"startLine": 5,
|
|
"endLine": 9
|
|
},
|
|
{
|
|
"startLine": 6,
|
|
"endLine": 7
|
|
}
|
|
]),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_rename() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("rename_did_open_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("rename_request.json"),
|
|
LspResponse::Request(
|
|
2,
|
|
json!({
|
|
"documentChanges": [{
|
|
"textDocument": {
|
|
"uri": "file:///a/file.ts",
|
|
"version": 1,
|
|
},
|
|
"edits": [{
|
|
"range": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 4
|
|
},
|
|
"end": {
|
|
"line": 0,
|
|
"character": 12
|
|
}
|
|
},
|
|
"newText": "variable_modified"
|
|
}, {
|
|
"range": {
|
|
"start": {
|
|
"line": 1,
|
|
"character": 12
|
|
},
|
|
"end": {
|
|
"line": 1,
|
|
"character": 20
|
|
}
|
|
},
|
|
"newText": "variable_modified"
|
|
}]
|
|
}]
|
|
}),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_selection_range() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("selection_range_did_open_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("selection_range_request.json"),
|
|
LspResponse::Request(
|
|
2,
|
|
json!([{
|
|
"range": {
|
|
"start": {
|
|
"line": 2,
|
|
"character": 8
|
|
},
|
|
"end": {
|
|
"line": 2,
|
|
"character": 9
|
|
}
|
|
},
|
|
"parent": {
|
|
"range": {
|
|
"start": {
|
|
"line": 2,
|
|
"character": 8
|
|
},
|
|
"end": {
|
|
"line": 2,
|
|
"character": 15
|
|
}
|
|
},
|
|
"parent": {
|
|
"range": {
|
|
"start": {
|
|
"line": 2,
|
|
"character": 4
|
|
},
|
|
"end": {
|
|
"line": 4,
|
|
"character": 5
|
|
}
|
|
},
|
|
"parent": {
|
|
"range": {
|
|
"start": {
|
|
"line": 1,
|
|
"character": 13
|
|
},
|
|
"end": {
|
|
"line": 6,
|
|
"character": 2
|
|
}
|
|
},
|
|
"parent": {
|
|
"range": {
|
|
"start": {
|
|
"line": 1,
|
|
"character": 2
|
|
},
|
|
"end": {
|
|
"line": 6,
|
|
"character": 3
|
|
}
|
|
},
|
|
"parent": {
|
|
"range": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 11
|
|
},
|
|
"end": {
|
|
"line": 7,
|
|
"character": 0
|
|
}
|
|
},
|
|
"parent": {
|
|
"range": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 0
|
|
},
|
|
"end": {
|
|
"line": 7,
|
|
"character": 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}]),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[rustfmt::skip]
|
|
async fn test_semantic_tokens() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(LspFixture::Path("initialize_request.json"), LspResponse::RequestAny),
|
|
(LspFixture::Path("initialized_notification.json"), LspResponse::None),
|
|
(
|
|
LspFixture::Path("semantic_tokens_did_open_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("semantic_tokens_full_request.json"),
|
|
LspResponse::Request(
|
|
2,
|
|
json!({
|
|
"data": [0, 5, 6, 1, 1, 0, 9, 6, 8, 9, 0, 8, 6, 8, 9, 2, 15 ,3, 10 ,5, 0, 4, 1, 6, 1, 0, 12 ,7, 2, 16 ,1, 8, 1, 7, 41 ,0, 4, 1, 6, 0, 0, 2, 5, 11 ,16 ,1, 9, 1, 7, 40 ,3, 10 ,4, 2, 1, 1, 11 ,1, 9, 9, 1, 2, 3, 11 ,1, 3, 6, 3, 0, 1, 0, 15 ,4, 2, 0, 1, 30 ,1, 6, 9, 1, 2, 3, 11 ,1, 1, 9, 9, 9, 3, 0, 16 ,3, 0, 0, 1, 17 ,12 ,11 ,3, 0, 24 ,3, 0, 0, 0, 4, 9, 9, 2]
|
|
}),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("semantic_tokens_range_request.json"),
|
|
LspResponse::Request(
|
|
4,
|
|
json!({
|
|
"data": [0, 5, 6, 1, 1, 0, 9, 6, 8, 9, 0, 8, 6, 8, 9, 2, 15 ,3, 10 ,5, 0, 4, 1, 6, 1, 0, 12 ,7, 2, 16 ,1, 8, 1, 7, 41 ,0, 4, 1, 6, 0, 0, 2, 5, 11 ,16 ,1, 9, 1, 7, 40]
|
|
}),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(LspFixture::Path("exit_notification.json"), LspResponse::None),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_code_lens_request() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_open_notification_cl_references.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("code_lens_request.json"),
|
|
LspResponse::Request(
|
|
2,
|
|
json!([
|
|
{
|
|
"range": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 6,
|
|
},
|
|
"end": {
|
|
"line": 0,
|
|
"character": 7,
|
|
}
|
|
},
|
|
"data": {
|
|
"specifier": "file:///a/file.ts",
|
|
"source": "references",
|
|
},
|
|
},
|
|
{
|
|
"range": {
|
|
"start": {
|
|
"line": 1,
|
|
"character": 2,
|
|
},
|
|
"end": {
|
|
"line": 1,
|
|
"character": 3,
|
|
}
|
|
},
|
|
"data": {
|
|
"specifier": "file:///a/file.ts",
|
|
"source": "references",
|
|
}
|
|
}
|
|
]),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("code_lens_resolve_request.json"),
|
|
LspResponse::Request(
|
|
4,
|
|
json!({
|
|
"range": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 6,
|
|
},
|
|
"end": {
|
|
"line": 0,
|
|
"character": 7,
|
|
}
|
|
},
|
|
"command": {
|
|
"title": "1 reference",
|
|
"command": "deno.showReferences",
|
|
"arguments": [
|
|
"file:///a/file.ts",
|
|
{
|
|
"line": 0,
|
|
"character": 6,
|
|
},
|
|
[
|
|
{
|
|
"uri": "file:///a/file.ts",
|
|
"range": {
|
|
"start": {
|
|
"line": 12,
|
|
"character": 14,
|
|
},
|
|
"end": {
|
|
"line": 12,
|
|
"character": 15,
|
|
}
|
|
}
|
|
}
|
|
],
|
|
]
|
|
}
|
|
}),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_signature_help() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("signature_help_did_open_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("signature_help_request_01.json"),
|
|
LspResponse::Request(
|
|
1,
|
|
json!({
|
|
"signatures": [
|
|
{
|
|
"label": "add(a: number, b: number): number",
|
|
"documentation": "Adds two numbers.",
|
|
"parameters": [
|
|
{
|
|
"label": "a: number",
|
|
"documentation": "This is a first number."
|
|
},
|
|
{
|
|
"label": "b: number",
|
|
"documentation": "This is a second number."
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"activeSignature": 0,
|
|
"activeParameter": 0
|
|
}),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("signature_help_did_change_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("signature_help_request_02.json"),
|
|
LspResponse::Request(
|
|
2,
|
|
json!({
|
|
"signatures": [
|
|
{
|
|
"label": "add(a: number, b: number): number",
|
|
"documentation": "Adds two numbers.",
|
|
"parameters": [
|
|
{
|
|
"label": "a: number",
|
|
"documentation": "This is a first number."
|
|
},
|
|
{
|
|
"label": "b: number",
|
|
"documentation": "This is a second number."
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"activeSignature": 0,
|
|
"activeParameter": 1
|
|
}),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_code_lens_impl_request() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_open_notification_cl_impl.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("code_lens_request.json"),
|
|
LspResponse::Request(
|
|
2,
|
|
json!([
|
|
{
|
|
"range": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 10,
|
|
},
|
|
"end": {
|
|
"line": 0,
|
|
"character": 11,
|
|
}
|
|
},
|
|
"data": {
|
|
"specifier": "file:///a/file.ts",
|
|
"source": "implementations",
|
|
},
|
|
},
|
|
{
|
|
"range": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 10,
|
|
},
|
|
"end": {
|
|
"line": 0,
|
|
"character": 11,
|
|
}
|
|
},
|
|
"data": {
|
|
"specifier": "file:///a/file.ts",
|
|
"source": "references",
|
|
},
|
|
},
|
|
{
|
|
"range": {
|
|
"start": {
|
|
"line": 4,
|
|
"character": 6,
|
|
},
|
|
"end": {
|
|
"line": 4,
|
|
"character": 7,
|
|
}
|
|
},
|
|
"data": {
|
|
"specifier": "file:///a/file.ts",
|
|
"source": "references",
|
|
},
|
|
},
|
|
]),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("code_lens_resolve_request_impl.json"),
|
|
LspResponse::Request(
|
|
4,
|
|
json!({
|
|
"range": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 10,
|
|
},
|
|
"end": {
|
|
"line": 0,
|
|
"character": 11,
|
|
}
|
|
},
|
|
"command": {
|
|
"title": "1 implementation",
|
|
"command": "deno.showReferences",
|
|
"arguments": [
|
|
"file:///a/file.ts",
|
|
{
|
|
"line": 0,
|
|
"character": 10,
|
|
},
|
|
[
|
|
{
|
|
"uri": "file:///a/file.ts",
|
|
"range": {
|
|
"start": {
|
|
"line": 4,
|
|
"character": 6,
|
|
},
|
|
"end": {
|
|
"line": 4,
|
|
"character": 7,
|
|
}
|
|
}
|
|
}
|
|
],
|
|
]
|
|
}
|
|
}),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct CodeLensResponse {
|
|
pub result: Option<Vec<CodeLens>>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct CodeLensResolveResponse {
|
|
pub result: CodeLens,
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_code_lens_non_doc_nav_tree() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_open_notification_asset.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("references_request_asset.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("virtual_text_document_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("code_lens_request_asset.json"),
|
|
LspResponse::RequestAssert(|value| {
|
|
let resp: CodeLensResponse = serde_json::from_value(value).unwrap();
|
|
let lenses = resp.result.unwrap();
|
|
assert!(lenses.len() > 50);
|
|
}),
|
|
),
|
|
(
|
|
LspFixture::Path("code_lens_resolve_request_asset.json"),
|
|
LspResponse::RequestAssert(|value| {
|
|
let resp: CodeLensResolveResponse =
|
|
serde_json::from_value(value).unwrap();
|
|
assert!(resp.result.command.is_some());
|
|
}),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_code_actions() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_open_notification_code_action.json"),
|
|
LspResponse::None,
|
|
),
|
|
(LspFixture::None, LspResponse::Delay(20000)),
|
|
(
|
|
LspFixture::Path("code_action_request.json"),
|
|
LspResponse::RequestFixture(2, "code_action_response.json".to_string()),
|
|
),
|
|
(
|
|
LspFixture::Path("code_action_resolve_request.json"),
|
|
LspResponse::RequestFixture(
|
|
4,
|
|
"code_action_resolve_request_response.json".to_string(),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_code_actions_deno_cache() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_open_notification_cache.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("code_action_request_cache.json"),
|
|
LspResponse::RequestFixture(
|
|
2,
|
|
"code_action_response_cache.json".to_string(),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct CompletionResult {
|
|
pub result: Option<CompletionResponse>,
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_completions() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_open_notification_completions.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("completion_request.json"),
|
|
LspResponse::RequestAssert(|value| {
|
|
let response: CompletionResult =
|
|
serde_json::from_value(value).unwrap();
|
|
let result = response.result.unwrap();
|
|
match result {
|
|
CompletionResponse::List(list) => {
|
|
// there should be at least 90 completions for `Deno.`
|
|
assert!(list.items.len() > 90);
|
|
}
|
|
_ => panic!("unexpected result"),
|
|
}
|
|
}),
|
|
),
|
|
(
|
|
LspFixture::Path("completion_resolve_request.json"),
|
|
LspResponse::Request(
|
|
4,
|
|
json!({
|
|
"label": "build",
|
|
"kind": 6,
|
|
"detail": "const Deno.build: {\n target: string;\n arch: \"x86_64\" | \"aarch64\";\n os: \"darwin\" | \"linux\" | \"windows\";\n vendor: string;\n env?: string | undefined;\n}",
|
|
"documentation": {
|
|
"kind": "markdown",
|
|
"value": "Build related information."
|
|
},
|
|
"sortText": "1",
|
|
"insertTextFormat": 1,
|
|
}),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_completions_optional() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_open_notification_completion_optional.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("completion_request_optional.json"),
|
|
LspResponse::Request(
|
|
2,
|
|
json!({
|
|
"isIncomplete": false,
|
|
"items": [
|
|
{
|
|
"label": "b?",
|
|
"kind": 5,
|
|
"sortText": "1",
|
|
"filterText": "b",
|
|
"insertText": "b",
|
|
"data": {
|
|
"tsc": {
|
|
"specifier": "file:///a/file.ts",
|
|
"position": 79,
|
|
"name": "b",
|
|
"useCodeSnippet": false
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("completion_resolve_request_optional.json"),
|
|
LspResponse::Request(
|
|
4,
|
|
json!({
|
|
"label": "b?",
|
|
"kind": 5,
|
|
"detail": "(property) A.b?: string | undefined",
|
|
"documentation": {
|
|
"kind": "markdown",
|
|
"value": ""
|
|
},
|
|
"sortText": "1",
|
|
"filterText": "b",
|
|
"insertText": "b"
|
|
}),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_completions_registry() {
|
|
let _g = test_util::http_server();
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request_registry.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_open_notification_completion_registry.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("completion_request_registry.json"),
|
|
LspResponse::RequestAssert(|value| {
|
|
let response: CompletionResult =
|
|
serde_json::from_value(value).unwrap();
|
|
let result = response.result.unwrap();
|
|
if let CompletionResponse::List(list) = result {
|
|
assert_eq!(list.items.len(), 3);
|
|
} else {
|
|
panic!("unexpected result");
|
|
}
|
|
}),
|
|
),
|
|
(
|
|
LspFixture::Path("completion_resolve_request_registry.json"),
|
|
LspResponse::Request(
|
|
4,
|
|
json!({
|
|
"label": "v2.0.0",
|
|
"kind": 19,
|
|
"detail": "(version)",
|
|
"sortText": "0000000003",
|
|
"filterText": "http://localhost:4545/x/a@v2.0.0",
|
|
"textEdit": {
|
|
"range": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 20
|
|
},
|
|
"end": {
|
|
"line": 0,
|
|
"character": 46
|
|
}
|
|
},
|
|
"newText": "http://localhost:4545/x/a@v2.0.0"
|
|
}
|
|
}),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_completion_registry_empty_specifier() {
|
|
let _g = test_util::http_server();
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request_registry.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_open_notification_completion_registry_02.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("completion_request_registry_02.json"),
|
|
LspResponse::Request(
|
|
2,
|
|
json!({
|
|
"isIncomplete": false,
|
|
"items": [
|
|
{
|
|
"label": ".",
|
|
"kind": 19,
|
|
"detail": "(local)",
|
|
"sortText": "1",
|
|
"insertText": "."
|
|
},
|
|
{
|
|
"label": "..",
|
|
"kind": 19,
|
|
"detail": "(local)",
|
|
"sortText": "1",
|
|
"insertText": ".."
|
|
},
|
|
{
|
|
"label": "http://localhost:4545",
|
|
"kind": 19,
|
|
"detail": "(registry)",
|
|
"sortText": "2",
|
|
"textEdit": {
|
|
"range": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 20
|
|
},
|
|
"end": {
|
|
"line": 0,
|
|
"character": 20
|
|
}
|
|
},
|
|
"newText": "http://localhost:4545"
|
|
}
|
|
}
|
|
]
|
|
}),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct PerformanceAverages {
|
|
averages: Vec<PerformanceAverage>,
|
|
}
|
|
#[derive(Deserialize)]
|
|
struct PerformanceResponse {
|
|
result: PerformanceAverages,
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_deno_performance_request() {
|
|
let mut harness = LspTestHarness::new(vec![
|
|
(
|
|
LspFixture::Path("initialize_request.json"),
|
|
LspResponse::RequestAny,
|
|
),
|
|
(
|
|
LspFixture::Path("initialized_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("did_open_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
(
|
|
LspFixture::Path("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
|
|
}
|
|
}
|
|
}),
|
|
),
|
|
),
|
|
(
|
|
LspFixture::Path("performance_request.json"),
|
|
LspResponse::RequestAssert(|value| {
|
|
let resp: PerformanceResponse =
|
|
serde_json::from_value(value).unwrap();
|
|
// the len can be variable since some of the parts of the language
|
|
// server run in separate threads and may not add to performance by
|
|
// the time the results are checked.
|
|
assert!(resp.result.averages.len() >= 6);
|
|
}),
|
|
),
|
|
(
|
|
LspFixture::Path("shutdown_request.json"),
|
|
LspResponse::Request(3, json!(null)),
|
|
),
|
|
(
|
|
LspFixture::Path("exit_notification.json"),
|
|
LspResponse::None,
|
|
),
|
|
]);
|
|
harness.run().await;
|
|
}
|
|
}
|