mirror of
https://github.com/denoland/deno.git
synced 2024-12-25 16:49:18 -05:00
feat(lsp): ts language service scopes (#24345)
This commit is contained in:
parent
2a2ff96be1
commit
67dcd6db51
8 changed files with 1265 additions and 357 deletions
|
@ -1,5 +1,7 @@
|
|||
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
use crate::lsp::logging::lsp_warn;
|
||||
|
||||
use super::analysis::source_range_to_lsp_range;
|
||||
use super::config::CodeLensSettings;
|
||||
use super::language_server;
|
||||
|
@ -27,6 +29,7 @@ use std::cell::RefCell;
|
|||
use std::collections::HashSet;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use tower_lsp::jsonrpc::Error as LspError;
|
||||
use tower_lsp::lsp_types as lsp;
|
||||
|
||||
static ABSTRACT_MODIFIER: Lazy<Regex> = lazy_regex!(r"\babstract\b");
|
||||
|
@ -260,7 +263,11 @@ async fn resolve_implementation_code_lens(
|
|||
data.specifier.clone(),
|
||||
line_index.offset_tsc(code_lens.range.start)?,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|err| {
|
||||
lsp_warn!("{err}");
|
||||
LspError::internal_error()
|
||||
})?;
|
||||
if let Some(implementations) = maybe_implementations {
|
||||
let mut locations = Vec::new();
|
||||
for implementation in implementations {
|
||||
|
@ -357,7 +364,11 @@ async fn resolve_references_code_lens(
|
|||
data.specifier.clone(),
|
||||
line_index.offset_tsc(code_lens.range.start)?,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|err| {
|
||||
lsp_warn!("Unable to find references: {err}");
|
||||
LspError::internal_error()
|
||||
})?;
|
||||
let locations = get_locations(maybe_referenced_symbols, language_server)?;
|
||||
let title = if locations.len() == 1 {
|
||||
"1 reference".to_string()
|
||||
|
|
|
@ -1055,7 +1055,6 @@ impl Default for LspTsConfig {
|
|||
"esModuleInterop": true,
|
||||
"experimentalDecorators": false,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react",
|
||||
"lib": ["deno.ns", "deno.window", "deno.unstable"],
|
||||
"module": "esnext",
|
||||
"moduleDetection": "force",
|
||||
|
@ -1569,16 +1568,10 @@ impl ConfigData {
|
|||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ConfigTree {
|
||||
first_folder: Option<ModuleSpecifier>,
|
||||
scopes: Arc<BTreeMap<ModuleSpecifier, Arc<ConfigData>>>,
|
||||
}
|
||||
|
||||
impl ConfigTree {
|
||||
pub fn root_ts_config(&self) -> Arc<LspTsConfig> {
|
||||
let root_data = self.first_folder.as_ref().and_then(|s| self.scopes.get(s));
|
||||
root_data.map(|d| d.ts_config.clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn scope_for_specifier(
|
||||
&self,
|
||||
specifier: &ModuleSpecifier,
|
||||
|
@ -1773,7 +1766,6 @@ impl ConfigTree {
|
|||
);
|
||||
}
|
||||
}
|
||||
self.first_folder.clone_from(&settings.first_folder);
|
||||
self.scopes = Arc::new(scopes);
|
||||
}
|
||||
|
||||
|
@ -1790,7 +1782,6 @@ impl ConfigTree {
|
|||
)
|
||||
.await,
|
||||
);
|
||||
self.first_folder = Some(scope.clone());
|
||||
self.scopes = Arc::new([(scope, data)].into_iter().collect());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -144,6 +144,20 @@ impl AssetOrDocument {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn file_referrer(&self) -> Option<&ModuleSpecifier> {
|
||||
match self {
|
||||
AssetOrDocument::Asset(_) => None,
|
||||
AssetOrDocument::Document(doc) => doc.file_referrer(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scope(&self) -> Option<&ModuleSpecifier> {
|
||||
match self {
|
||||
AssetOrDocument::Asset(_) => None,
|
||||
AssetOrDocument::Document(doc) => doc.scope(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maybe_semantic_tokens(&self) -> Option<lsp::SemanticTokens> {
|
||||
match self {
|
||||
AssetOrDocument::Asset(_) => None,
|
||||
|
@ -605,6 +619,13 @@ impl Document {
|
|||
self.file_referrer.as_ref()
|
||||
}
|
||||
|
||||
pub fn scope(&self) -> Option<&ModuleSpecifier> {
|
||||
self
|
||||
.file_referrer
|
||||
.as_ref()
|
||||
.and_then(|r| self.config.tree.scope_for_specifier(r))
|
||||
}
|
||||
|
||||
pub fn content(&self) -> &Arc<str> {
|
||||
&self.text
|
||||
}
|
||||
|
@ -926,9 +947,9 @@ pub struct Documents {
|
|||
/// The npm package requirements found in npm specifiers.
|
||||
npm_reqs_by_scope:
|
||||
Arc<BTreeMap<Option<ModuleSpecifier>, BTreeSet<PackageReq>>>,
|
||||
/// Gets if any document had a node: specifier such that a @types/node package
|
||||
/// should be injected.
|
||||
has_injected_types_node_package: bool,
|
||||
/// Config scopes that contain a node: specifier such that a @types/node
|
||||
/// package should be injected.
|
||||
scopes_with_node_specifier: Arc<HashSet<Option<ModuleSpecifier>>>,
|
||||
}
|
||||
|
||||
impl Documents {
|
||||
|
@ -1122,10 +1143,10 @@ impl Documents {
|
|||
self.npm_reqs_by_scope.clone()
|
||||
}
|
||||
|
||||
/// Returns if a @types/node package was injected into the npm
|
||||
/// resolver based on the state of the documents.
|
||||
pub fn has_injected_types_node_package(&self) -> bool {
|
||||
self.has_injected_types_node_package
|
||||
pub fn scopes_with_node_specifier(
|
||||
&self,
|
||||
) -> &Arc<HashSet<Option<ModuleSpecifier>>> {
|
||||
&self.scopes_with_node_specifier
|
||||
}
|
||||
|
||||
/// Return a document for the specifier.
|
||||
|
@ -1346,20 +1367,18 @@ impl Documents {
|
|||
/// document.
|
||||
fn calculate_npm_reqs_if_dirty(&mut self) {
|
||||
let mut npm_reqs_by_scope: BTreeMap<_, BTreeSet<_>> = Default::default();
|
||||
let mut scopes_with_node_builtin_specifier = HashSet::new();
|
||||
let mut scopes_with_specifier = HashSet::new();
|
||||
let is_fs_docs_dirty = self.file_system_docs.set_dirty(false);
|
||||
if !is_fs_docs_dirty && !self.dirty {
|
||||
return;
|
||||
}
|
||||
let mut visit_doc = |doc: &Arc<Document>| {
|
||||
let scope = doc
|
||||
.file_referrer()
|
||||
.and_then(|r| self.config.tree.scope_for_specifier(r));
|
||||
let scope = doc.scope();
|
||||
let reqs = npm_reqs_by_scope.entry(scope.cloned()).or_default();
|
||||
for dependency in doc.dependencies().values() {
|
||||
if let Some(dep) = dependency.get_code() {
|
||||
if dep.scheme() == "node" {
|
||||
scopes_with_node_builtin_specifier.insert(scope.cloned());
|
||||
scopes_with_specifier.insert(scope.cloned());
|
||||
}
|
||||
if let Ok(reference) = NpmPackageReqReference::from_specifier(dep) {
|
||||
reqs.insert(reference.into_inner().req);
|
||||
|
@ -1402,15 +1421,15 @@ impl Documents {
|
|||
// Ensure a @types/node package exists when any module uses a node: specifier.
|
||||
// Unlike on the command line, here we just add @types/node to the npm package
|
||||
// requirements since this won't end up in the lockfile.
|
||||
for scope in scopes_with_node_builtin_specifier {
|
||||
let reqs = npm_reqs_by_scope.entry(scope).or_default();
|
||||
for scope in &scopes_with_specifier {
|
||||
let reqs = npm_reqs_by_scope.entry(scope.clone()).or_default();
|
||||
if !reqs.iter().any(|r| r.name == "@types/node") {
|
||||
self.has_injected_types_node_package = true;
|
||||
reqs.insert(PackageReq::from_str("@types/node").unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
self.npm_reqs_by_scope = Arc::new(npm_reqs_by_scope);
|
||||
self.scopes_with_node_specifier = Arc::new(scopes_with_specifier);
|
||||
self.dirty = false;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ use deno_graph::Resolution;
|
|||
use deno_runtime::deno_tls::rustls::RootCertStore;
|
||||
use deno_runtime::deno_tls::RootCertStoreProvider;
|
||||
use deno_semver::jsr::JsrPackageReqReference;
|
||||
use indexmap::Equivalent;
|
||||
use indexmap::IndexSet;
|
||||
use log::error;
|
||||
use serde::Deserialize;
|
||||
|
@ -570,7 +571,11 @@ impl Inner {
|
|||
} else {
|
||||
let navigation_tree: tsc::NavigationTree = self
|
||||
.ts_server
|
||||
.get_navigation_tree(self.snapshot(), specifier.clone())
|
||||
.get_navigation_tree(
|
||||
self.snapshot(),
|
||||
specifier.clone(),
|
||||
asset_or_doc.scope().cloned(),
|
||||
)
|
||||
.await?;
|
||||
let navigation_tree = Arc::new(navigation_tree);
|
||||
match asset_or_doc {
|
||||
|
@ -1065,8 +1070,8 @@ impl Inner {
|
|||
params.text_document.text.into(),
|
||||
file_referrer,
|
||||
);
|
||||
self.project_changed([(document.specifier(), ChangeKind::Opened)], false);
|
||||
if document.is_diagnosable() {
|
||||
self.project_changed([(document.specifier(), ChangeKind::Opened)], false);
|
||||
self.refresh_npm_specifiers().await;
|
||||
self.diagnostics_server.invalidate(&[specifier]);
|
||||
self.send_diagnostics_update();
|
||||
|
@ -1087,11 +1092,21 @@ impl Inner {
|
|||
) {
|
||||
Ok(document) => {
|
||||
if document.is_diagnosable() {
|
||||
let old_scopes_with_node_specifier =
|
||||
self.documents.scopes_with_node_specifier().clone();
|
||||
self.refresh_npm_specifiers().await;
|
||||
let mut config_changed = false;
|
||||
if !self
|
||||
.documents
|
||||
.scopes_with_node_specifier()
|
||||
.equivalent(&old_scopes_with_node_specifier)
|
||||
{
|
||||
config_changed = true;
|
||||
}
|
||||
self.project_changed(
|
||||
[(document.specifier(), ChangeKind::Modified)],
|
||||
false,
|
||||
config_changed,
|
||||
);
|
||||
self.refresh_npm_specifiers().await;
|
||||
self.diagnostics_server.invalidate(&[specifier]);
|
||||
self.send_diagnostics_update();
|
||||
self.send_testing_update();
|
||||
|
@ -1399,7 +1414,7 @@ impl Inner {
|
|||
|
||||
let mark = self.performance.mark_with_args("lsp.hover", ¶ms);
|
||||
let asset_or_doc = self.get_asset_or_document(&specifier)?;
|
||||
let file_referrer = asset_or_doc.document().and_then(|d| d.file_referrer());
|
||||
let file_referrer = asset_or_doc.file_referrer();
|
||||
let hover = if let Some((_, dep, range)) = asset_or_doc
|
||||
.get_maybe_dependency(¶ms.text_document_position_params.position)
|
||||
{
|
||||
|
@ -1459,7 +1474,12 @@ impl Inner {
|
|||
line_index.offset_tsc(params.text_document_position_params.position)?;
|
||||
let maybe_quick_info = self
|
||||
.ts_server
|
||||
.get_quick_info(self.snapshot(), specifier.clone(), position)
|
||||
.get_quick_info(
|
||||
self.snapshot(),
|
||||
specifier.clone(),
|
||||
position,
|
||||
asset_or_doc.scope().cloned(),
|
||||
)
|
||||
.await?;
|
||||
maybe_quick_info.map(|qi| qi.to_hover(line_index, self))
|
||||
};
|
||||
|
@ -1588,6 +1608,7 @@ impl Inner {
|
|||
&self.config,
|
||||
&specifier,
|
||||
),
|
||||
asset_or_doc.scope().cloned(),
|
||||
)
|
||||
.await;
|
||||
for action in actions {
|
||||
|
@ -1682,6 +1703,7 @@ impl Inner {
|
|||
)),
|
||||
params.context.trigger_kind,
|
||||
only,
|
||||
asset_or_doc.scope().cloned(),
|
||||
)
|
||||
.await?;
|
||||
let mut refactor_actions = Vec::<CodeAction>::new();
|
||||
|
@ -1732,6 +1754,10 @@ impl Inner {
|
|||
error!("Unable to decode code action data: {:#}", err);
|
||||
LspError::invalid_params("The CodeAction's data is invalid.")
|
||||
})?;
|
||||
let scope = self
|
||||
.get_asset_or_document(&code_action_data.specifier)
|
||||
.ok()
|
||||
.and_then(|d| d.scope().cloned());
|
||||
let combined_code_actions = self
|
||||
.ts_server
|
||||
.get_combined_code_fix(
|
||||
|
@ -1747,6 +1773,7 @@ impl Inner {
|
|||
&self.config,
|
||||
&code_action_data.specifier,
|
||||
),
|
||||
scope,
|
||||
)
|
||||
.await?;
|
||||
if combined_code_actions.commands.is_some() {
|
||||
|
@ -1801,6 +1828,7 @@ impl Inner {
|
|||
&self.config,
|
||||
&action_data.specifier,
|
||||
)),
|
||||
asset_or_doc.scope().cloned(),
|
||||
)
|
||||
.await?;
|
||||
code_action.edit = refactor_edit_info.to_workspace_edit(self)?;
|
||||
|
@ -1944,6 +1972,7 @@ impl Inner {
|
|||
specifier,
|
||||
line_index.offset_tsc(params.text_document_position_params.position)?,
|
||||
files_to_search,
|
||||
asset_or_doc.scope().cloned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
@ -1984,7 +2013,11 @@ impl Inner {
|
|||
specifier.clone(),
|
||||
line_index.offset_tsc(params.text_document_position.position)?,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|err| {
|
||||
lsp_warn!("Unable to find references: {err}");
|
||||
LspError::internal_error()
|
||||
})?;
|
||||
|
||||
if let Some(symbols) = maybe_referenced_symbols {
|
||||
let mut results = Vec::new();
|
||||
|
@ -2037,6 +2070,7 @@ impl Inner {
|
|||
self.snapshot(),
|
||||
specifier,
|
||||
line_index.offset_tsc(params.text_document_position_params.position)?,
|
||||
asset_or_doc.scope().cloned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
@ -2075,6 +2109,7 @@ impl Inner {
|
|||
self.snapshot(),
|
||||
specifier,
|
||||
line_index.offset_tsc(params.text_document_position_params.position)?,
|
||||
asset_or_doc.scope().cloned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
@ -2123,10 +2158,7 @@ impl Inner {
|
|||
.map(|s| s.suggest.include_completions_for_import_statements)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
let file_referrer = asset_or_doc
|
||||
.document()
|
||||
.and_then(|d| d.file_referrer())
|
||||
.unwrap_or(&specifier);
|
||||
let file_referrer = asset_or_doc.file_referrer().unwrap_or(&specifier);
|
||||
response = completions::get_import_completions(
|
||||
&specifier,
|
||||
¶ms.text_document_position.position,
|
||||
|
@ -2158,6 +2190,7 @@ impl Inner {
|
|||
};
|
||||
let position =
|
||||
line_index.offset_tsc(params.text_document_position.position)?;
|
||||
let scope = asset_or_doc.scope();
|
||||
let maybe_completion_info = self
|
||||
.ts_server
|
||||
.get_completions(
|
||||
|
@ -2178,6 +2211,7 @@ impl Inner {
|
|||
.fmt_options_for_specifier(&specifier)
|
||||
.options)
|
||||
.into(),
|
||||
scope.cloned(),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
@ -2219,6 +2253,10 @@ impl Inner {
|
|||
})?;
|
||||
if let Some(data) = &data.tsc {
|
||||
let specifier = &data.specifier;
|
||||
let scope = self
|
||||
.get_asset_or_document(specifier)
|
||||
.ok()
|
||||
.and_then(|d| d.scope().cloned());
|
||||
let result = self
|
||||
.ts_server
|
||||
.get_completion_details(
|
||||
|
@ -2240,6 +2278,7 @@ impl Inner {
|
|||
),
|
||||
..data.into()
|
||||
},
|
||||
scope,
|
||||
)
|
||||
.await;
|
||||
match result {
|
||||
|
@ -2309,7 +2348,11 @@ impl Inner {
|
|||
specifier,
|
||||
line_index.offset_tsc(params.text_document_position_params.position)?,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|err| {
|
||||
lsp_warn!("{:#}", err);
|
||||
LspError::internal_error()
|
||||
})?;
|
||||
|
||||
let result = if let Some(implementations) = maybe_implementations {
|
||||
let mut links = Vec::new();
|
||||
|
@ -2347,7 +2390,11 @@ impl Inner {
|
|||
|
||||
let outlining_spans = self
|
||||
.ts_server
|
||||
.get_outlining_spans(self.snapshot(), specifier)
|
||||
.get_outlining_spans(
|
||||
self.snapshot(),
|
||||
specifier,
|
||||
asset_or_doc.scope().cloned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let response = if !outlining_spans.is_empty() {
|
||||
|
@ -2396,7 +2443,11 @@ impl Inner {
|
|||
specifier,
|
||||
line_index.offset_tsc(params.item.selection_range.start)?,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|err| {
|
||||
lsp_warn!("{:#}", err);
|
||||
LspError::internal_error()
|
||||
})?;
|
||||
|
||||
let maybe_root_path_owned = self
|
||||
.config
|
||||
|
@ -2440,6 +2491,7 @@ impl Inner {
|
|||
self.snapshot(),
|
||||
specifier,
|
||||
line_index.offset_tsc(params.item.selection_range.start)?,
|
||||
asset_or_doc.scope().cloned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
@ -2487,6 +2539,7 @@ impl Inner {
|
|||
self.snapshot(),
|
||||
specifier,
|
||||
line_index.offset_tsc(params.text_document_position_params.position)?,
|
||||
asset_or_doc.scope().cloned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
@ -2549,7 +2602,11 @@ impl Inner {
|
|||
specifier,
|
||||
line_index.offset_tsc(params.text_document_position.position)?,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|err| {
|
||||
lsp_warn!("{:#}", err);
|
||||
LspError::internal_error()
|
||||
})?;
|
||||
|
||||
if let Some(locations) = maybe_locations {
|
||||
let rename_locations = tsc::RenameLocations { locations };
|
||||
|
@ -2594,6 +2651,7 @@ impl Inner {
|
|||
self.snapshot(),
|
||||
specifier.clone(),
|
||||
line_index.offset_tsc(position)?,
|
||||
asset_or_doc.scope().cloned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
@ -2637,6 +2695,7 @@ impl Inner {
|
|||
self.snapshot(),
|
||||
specifier,
|
||||
0..line_index.text_content_length_utf16().into(),
|
||||
asset_or_doc.scope().cloned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
@ -2692,6 +2751,7 @@ impl Inner {
|
|||
specifier,
|
||||
line_index.offset_tsc(params.range.start)?
|
||||
..line_index.offset_tsc(params.range.end)?,
|
||||
asset_or_doc.scope().cloned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
@ -2744,6 +2804,7 @@ impl Inner {
|
|||
specifier,
|
||||
line_index.offset_tsc(params.text_document_position_params.position)?,
|
||||
options,
|
||||
asset_or_doc.scope().cloned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
@ -2799,7 +2860,11 @@ impl Inner {
|
|||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?,
|
||||
.await
|
||||
.map_err(|err| {
|
||||
lsp_warn!("{:#}", err);
|
||||
LspError::internal_error()
|
||||
})?,
|
||||
);
|
||||
}
|
||||
file_text_changes_to_workspace_edit(&changes, self)
|
||||
|
@ -2822,7 +2887,11 @@ impl Inner {
|
|||
file: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("{:#}", err);
|
||||
LspError::invalid_request()
|
||||
})?;
|
||||
|
||||
let maybe_symbol_information = if navigate_to_items.is_empty() {
|
||||
None
|
||||
|
@ -2849,7 +2918,15 @@ impl Inner {
|
|||
self.ts_server.project_changed(
|
||||
self.snapshot(),
|
||||
modified_scripts,
|
||||
config_changed,
|
||||
config_changed.then(|| {
|
||||
self
|
||||
.config
|
||||
.tree
|
||||
.data_by_scope()
|
||||
.iter()
|
||||
.map(|(s, d)| (s.clone(), d.ts_config.clone()))
|
||||
.collect()
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -3582,6 +3659,7 @@ impl Inner {
|
|||
&self.config,
|
||||
&specifier,
|
||||
),
|
||||
asset_or_doc.scope().cloned(),
|
||||
)
|
||||
.await?;
|
||||
let maybe_inlay_hints = maybe_inlay_hints.map(|hints| {
|
||||
|
|
|
@ -271,20 +271,20 @@ impl LspResolver {
|
|||
|
||||
pub fn graph_imports_by_referrer(
|
||||
&self,
|
||||
file_referrer: &ModuleSpecifier,
|
||||
) -> IndexMap<&ModuleSpecifier, Vec<&ModuleSpecifier>> {
|
||||
self
|
||||
.by_scope
|
||||
let resolver = self.get_scope_resolver(Some(file_referrer));
|
||||
resolver
|
||||
.graph_imports
|
||||
.iter()
|
||||
.flat_map(|(_, r)| {
|
||||
r.graph_imports.iter().map(|(s, i)| {
|
||||
(
|
||||
s,
|
||||
i.dependencies
|
||||
.values()
|
||||
.flat_map(|d| d.get_type().or_else(|| d.get_code()))
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.map(|(s, i)| {
|
||||
(
|
||||
s,
|
||||
i.dependencies
|
||||
.values()
|
||||
.flat_map(|d| d.get_type().or_else(|| d.get_code()))
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
|
715
cli/lsp/tsc.rs
715
cli/lsp/tsc.rs
File diff suppressed because it is too large
Load diff
|
@ -161,9 +161,6 @@ delete Object.prototype.__proto__;
|
|||
/** @type {Map<string, number>} */
|
||||
const sourceRefCounts = new Map();
|
||||
|
||||
/** @type {string[]=} */
|
||||
let scriptFileNamesCache;
|
||||
|
||||
/** @type {Map<string, string>} */
|
||||
const scriptVersionCache = new Map();
|
||||
|
||||
|
@ -172,14 +169,15 @@ delete Object.prototype.__proto__;
|
|||
|
||||
const isCjsCache = new SpecifierIsCjsCache();
|
||||
|
||||
/** @type {ts.CompilerOptions | null} */
|
||||
let tsConfigCache = null;
|
||||
|
||||
/** @type {number | null} */
|
||||
let projectVersionCache = null;
|
||||
|
||||
/** @type {string | null} */
|
||||
let lastRequestMethod = null;
|
||||
|
||||
/** @type {string | null} */
|
||||
let lastRequestScope = null;
|
||||
|
||||
const ChangeKind = {
|
||||
Opened: 0,
|
||||
Modified: 1,
|
||||
|
@ -542,8 +540,19 @@ delete Object.prototype.__proto__;
|
|||
}
|
||||
}
|
||||
|
||||
/** @type {ts.LanguageService & { [k:string]: any }} */
|
||||
let languageService;
|
||||
/** @typedef {{
|
||||
* ls: ts.LanguageService & { [k:string]: any },
|
||||
* compilerOptions: ts.CompilerOptions,
|
||||
* }} LanguageServiceEntry */
|
||||
/** @type {{ unscoped: LanguageServiceEntry, byScope: Map<string, LanguageServiceEntry> }} */
|
||||
const languageServiceEntries = {
|
||||
// @ts-ignore Will be set later.
|
||||
unscoped: null,
|
||||
byScope: new Map(),
|
||||
};
|
||||
|
||||
/** @type {{ unscoped: string[], byScope: Map<string, string[]> } | null} */
|
||||
let scriptNamesCache = null;
|
||||
|
||||
/** An object literal of the incremental compiler host, which provides the
|
||||
* specific "bindings" to the Deno environment that tsc needs to work.
|
||||
|
@ -785,32 +794,24 @@ delete Object.prototype.__proto__;
|
|||
if (logDebug) {
|
||||
debug("host.getCompilationSettings()");
|
||||
}
|
||||
if (tsConfigCache) {
|
||||
return tsConfigCache;
|
||||
}
|
||||
const tsConfig = normalizeConfig(ops.op_ts_config());
|
||||
const { options, errors } = ts
|
||||
.convertCompilerOptionsFromJson(tsConfig, "");
|
||||
Object.assign(options, {
|
||||
allowNonTsExtensions: true,
|
||||
allowImportingTsExtensions: true,
|
||||
});
|
||||
if (errors.length > 0 && logDebug) {
|
||||
debug(ts.formatDiagnostics(errors, host));
|
||||
}
|
||||
tsConfigCache = options;
|
||||
return options;
|
||||
return (lastRequestScope
|
||||
? languageServiceEntries.byScope.get(lastRequestScope)?.compilerOptions
|
||||
: null) ?? languageServiceEntries.unscoped.compilerOptions;
|
||||
},
|
||||
getScriptFileNames() {
|
||||
if (logDebug) {
|
||||
debug("host.getScriptFileNames()");
|
||||
}
|
||||
// tsc requests the script file names multiple times even though it can't
|
||||
// possibly have changed, so we will memoize it on a per request basis.
|
||||
if (scriptFileNamesCache) {
|
||||
return scriptFileNamesCache;
|
||||
if (!scriptNamesCache) {
|
||||
const { unscoped, byScope } = ops.op_script_names();
|
||||
scriptNamesCache = {
|
||||
unscoped,
|
||||
byScope: new Map(Object.entries(byScope)),
|
||||
};
|
||||
}
|
||||
return scriptFileNamesCache = ops.op_script_names();
|
||||
return (lastRequestScope
|
||||
? scriptNamesCache.byScope.get(lastRequestScope)
|
||||
: null) ?? scriptNamesCache.unscoped;
|
||||
},
|
||||
getScriptVersion(specifier) {
|
||||
if (logDebug) {
|
||||
|
@ -953,7 +954,7 @@ delete Object.prototype.__proto__;
|
|||
}
|
||||
}
|
||||
|
||||
/** @param {Record<string, string>} config */
|
||||
/** @param {Record<string, unknown>} config */
|
||||
function normalizeConfig(config) {
|
||||
// the typescript compiler doesn't know about the precompile
|
||||
// transform at the moment, so just tell it we're using react-jsx
|
||||
|
@ -966,6 +967,21 @@ delete Object.prototype.__proto__;
|
|||
return config;
|
||||
}
|
||||
|
||||
/** @param {Record<string, unknown>} config */
|
||||
function lspTsConfigToCompilerOptions(config) {
|
||||
const normalizedConfig = normalizeConfig(config);
|
||||
const { options, errors } = ts
|
||||
.convertCompilerOptionsFromJson(normalizedConfig, "");
|
||||
Object.assign(options, {
|
||||
allowNonTsExtensions: true,
|
||||
allowImportingTsExtensions: true,
|
||||
});
|
||||
if (errors.length > 0 && logDebug) {
|
||||
debug(ts.formatDiagnostics(errors, host));
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
/** The API that is called by Rust when executing a request.
|
||||
* @param {Request} request
|
||||
*/
|
||||
|
@ -1079,7 +1095,7 @@ delete Object.prototype.__proto__;
|
|||
/**
|
||||
* @param {number} _id
|
||||
* @param {any} data
|
||||
* @param {any | null} error
|
||||
* @param {string | null} error
|
||||
*/
|
||||
// TODO(bartlomieju): this feels needlessly generic, both type chcking
|
||||
// and language server use it with inefficient serialization. Id is not used
|
||||
|
@ -1088,19 +1104,19 @@ delete Object.prototype.__proto__;
|
|||
if (error) {
|
||||
ops.op_respond(
|
||||
"error",
|
||||
"stack" in error ? error.stack.toString() : error.toString(),
|
||||
error,
|
||||
);
|
||||
} else {
|
||||
ops.op_respond(JSON.stringify(data), "");
|
||||
}
|
||||
}
|
||||
|
||||
/** @typedef {[[string, number][], number, boolean] } PendingChange */
|
||||
/** @typedef {[[string, number][], number, [string, any][]] } PendingChange */
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {T | null} Option<T> */
|
||||
|
||||
/** @returns {Promise<[number, string, any[], Option<PendingChange>] | null>} */
|
||||
/** @returns {Promise<[number, string, any[], string | null, Option<PendingChange>] | null>} */
|
||||
async function pollRequests() {
|
||||
return await ops.op_poll_requests();
|
||||
}
|
||||
|
@ -1113,7 +1129,30 @@ delete Object.prototype.__proto__;
|
|||
throw new Error("The language server has already been initialized.");
|
||||
}
|
||||
hasStarted = true;
|
||||
languageService = ts.createLanguageService(host, documentRegistry);
|
||||
languageServiceEntries.unscoped = {
|
||||
ls: ts.createLanguageService(
|
||||
host,
|
||||
documentRegistry,
|
||||
),
|
||||
compilerOptions: lspTsConfigToCompilerOptions({
|
||||
"allowJs": true,
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": false,
|
||||
"isolatedModules": true,
|
||||
"lib": ["deno.ns", "deno.window", "deno.unstable"],
|
||||
"module": "esnext",
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"target": "esnext",
|
||||
"useDefineForClassFields": true,
|
||||
"useUnknownInCatchVariables": false,
|
||||
"jsx": "react",
|
||||
"jsxFactory": "React.createElement",
|
||||
"jsxFragmentFactory": "React.Fragment",
|
||||
}),
|
||||
};
|
||||
setLogDebug(enableDebugLogging, "TSLS");
|
||||
debug("serverInit()");
|
||||
|
||||
|
@ -1123,39 +1162,68 @@ delete Object.prototype.__proto__;
|
|||
break;
|
||||
}
|
||||
try {
|
||||
serverRequest(request[0], request[1], request[2], request[3]);
|
||||
} catch (err) {
|
||||
const reqString = "[" + request.map((v) =>
|
||||
JSON.stringify(v)
|
||||
).join(", ") + "]";
|
||||
error(
|
||||
`Error occurred processing request ${reqString} : ${
|
||||
"stack" in err ? err.stack : err
|
||||
}`,
|
||||
serverRequest(
|
||||
request[0],
|
||||
request[1],
|
||||
request[2],
|
||||
request[3],
|
||||
request[4],
|
||||
);
|
||||
} catch (err) {
|
||||
error(`Internal error occurred processing request: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} error
|
||||
* @param {any[] | null} args
|
||||
*/
|
||||
function formatErrorWithArgs(error, args) {
|
||||
let errorString = "stack" in error
|
||||
? error.stack.toString()
|
||||
: error.toString();
|
||||
if (args) {
|
||||
errorString += `\nFor request: [${
|
||||
args.map((v) => JSON.stringify(v)).join(", ")
|
||||
}]`;
|
||||
}
|
||||
return errorString;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @param {string} method
|
||||
* @param {any[]} args
|
||||
* @param {string | null} scope
|
||||
* @param {PendingChange | null} maybeChange
|
||||
*/
|
||||
function serverRequest(id, method, args, maybeChange) {
|
||||
function serverRequest(id, method, args, scope, maybeChange) {
|
||||
if (logDebug) {
|
||||
debug(`serverRequest()`, id, method, args, maybeChange);
|
||||
debug(`serverRequest()`, id, method, args, scope, maybeChange);
|
||||
}
|
||||
lastRequestMethod = method;
|
||||
if (maybeChange !== null) {
|
||||
const changedScripts = maybeChange[0];
|
||||
const newProjectVersion = maybeChange[1];
|
||||
const configChanged = maybeChange[2];
|
||||
|
||||
if (configChanged) {
|
||||
tsConfigCache = null;
|
||||
const newConfigsByScope = maybeChange[2];
|
||||
if (newConfigsByScope) {
|
||||
isNodeSourceFileCache.clear();
|
||||
/** @type { typeof languageServiceEntries.byScope } */
|
||||
const newByScope = new Map();
|
||||
for (const [scope, config] of newConfigsByScope) {
|
||||
lastRequestScope = scope;
|
||||
const oldEntry = languageServiceEntries.byScope.get(scope);
|
||||
const ls = oldEntry
|
||||
? oldEntry.ls
|
||||
: ts.createLanguageService(host, documentRegistry);
|
||||
const compilerOptions = lspTsConfigToCompilerOptions(config);
|
||||
newByScope.set(scope, { ls, compilerOptions });
|
||||
languageServiceEntries.byScope.delete(scope);
|
||||
}
|
||||
for (const oldEntry of languageServiceEntries.byScope.values()) {
|
||||
oldEntry.ls.dispose();
|
||||
}
|
||||
languageServiceEntries.byScope = newByScope;
|
||||
}
|
||||
|
||||
projectVersionCache = newProjectVersion;
|
||||
|
@ -1172,10 +1240,15 @@ delete Object.prototype.__proto__;
|
|||
sourceTextCache.delete(script);
|
||||
}
|
||||
|
||||
if (configChanged || opened || closed) {
|
||||
scriptFileNamesCache = undefined;
|
||||
if (newConfigsByScope || opened || closed) {
|
||||
scriptNamesCache = null;
|
||||
}
|
||||
}
|
||||
|
||||
lastRequestMethod = method;
|
||||
lastRequestScope = scope;
|
||||
const ls = (scope ? languageServiceEntries.byScope.get(scope)?.ls : null) ??
|
||||
languageServiceEntries.unscoped.ls;
|
||||
switch (method) {
|
||||
case "$getSupportedCodeFixes": {
|
||||
return respond(
|
||||
|
@ -1200,9 +1273,9 @@ delete Object.prototype.__proto__;
|
|||
const diagnosticMap = {};
|
||||
for (const specifier of args[0]) {
|
||||
diagnosticMap[specifier] = fromTypeScriptDiagnostics([
|
||||
...languageService.getSemanticDiagnostics(specifier),
|
||||
...languageService.getSuggestionDiagnostics(specifier),
|
||||
...languageService.getSyntacticDiagnostics(specifier),
|
||||
...ls.getSemanticDiagnostics(specifier),
|
||||
...ls.getSuggestionDiagnostics(specifier),
|
||||
...ls.getSyntacticDiagnostics(specifier),
|
||||
].filter(({ code }) => !IGNORED_DIAGNOSTICS.includes(code)));
|
||||
}
|
||||
return respond(id, diagnosticMap);
|
||||
|
@ -1210,25 +1283,31 @@ delete Object.prototype.__proto__;
|
|||
if (
|
||||
!isCancellationError(e)
|
||||
) {
|
||||
respond(id, {}, e);
|
||||
throw e;
|
||||
return respond(
|
||||
id,
|
||||
{},
|
||||
formatErrorWithArgs(e, [id, method, args, scope, maybeChange]),
|
||||
);
|
||||
}
|
||||
return respond(id, {});
|
||||
}
|
||||
}
|
||||
default:
|
||||
if (typeof languageService[method] === "function") {
|
||||
if (typeof ls[method] === "function") {
|
||||
// The `getCompletionEntryDetails()` method returns null if the
|
||||
// `source` is `null` for whatever reason. It must be `undefined`.
|
||||
if (method == "getCompletionEntryDetails") {
|
||||
args[4] ??= undefined;
|
||||
}
|
||||
try {
|
||||
return respond(id, languageService[method](...args));
|
||||
return respond(id, ls[method](...args));
|
||||
} catch (e) {
|
||||
if (!isCancellationError(e)) {
|
||||
respond(id, null, e);
|
||||
throw e;
|
||||
return respond(
|
||||
id,
|
||||
null,
|
||||
formatErrorWithArgs(e, [id, method, args, scope, maybeChange]),
|
||||
);
|
||||
}
|
||||
return respond(id);
|
||||
}
|
||||
|
|
|
@ -9603,7 +9603,6 @@ fn lsp_performance() {
|
|||
"tsc.op.op_is_node_file",
|
||||
"tsc.op.op_load",
|
||||
"tsc.op.op_script_names",
|
||||
"tsc.op.op_ts_config",
|
||||
"tsc.request.$getAssets",
|
||||
"tsc.request.$getSupportedCodeFixes",
|
||||
"tsc.request.getQuickInfoAtPosition",
|
||||
|
@ -12431,6 +12430,500 @@ fn lsp_deno_json_scopes_vendor_dirs() {
|
|||
client.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_deno_json_scopes_ts_config() {
|
||||
let context = TestContextBuilder::new().use_temp_cwd().build();
|
||||
let temp_dir = context.temp_dir();
|
||||
temp_dir.create_dir_all("project1");
|
||||
temp_dir.create_dir_all("project2");
|
||||
temp_dir.write("project1/deno.json", json!({}).to_string());
|
||||
temp_dir.write(
|
||||
"project2/deno.json",
|
||||
json!({
|
||||
"compilerOptions": {
|
||||
"lib": ["deno.worker"],
|
||||
},
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
let mut client = context.new_lsp_command().build();
|
||||
client.initialize_default();
|
||||
client.did_open(json!({
|
||||
"textDocument": {
|
||||
"uri": temp_dir.uri().join("project1/file.ts").unwrap(),
|
||||
"languageId": "typescript",
|
||||
"version": 1,
|
||||
"text": "Window;\nWorkerGlobalScope;\n",
|
||||
},
|
||||
}));
|
||||
let diagnostics = client.did_open(json!({
|
||||
"textDocument": {
|
||||
"uri": temp_dir.uri().join("project2/file.ts").unwrap(),
|
||||
"languageId": "typescript",
|
||||
"version": 1,
|
||||
"text": "Window;\nWorkerGlobalScope;\n",
|
||||
},
|
||||
}));
|
||||
assert_eq!(
|
||||
json!(diagnostics.all_messages()),
|
||||
json!([
|
||||
{
|
||||
"uri": temp_dir.uri().join("project2/file.ts").unwrap(),
|
||||
"version": 1,
|
||||
"diagnostics": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 6 },
|
||||
},
|
||||
"severity": 1,
|
||||
"code": 2304,
|
||||
"source": "deno-ts",
|
||||
"message": "Cannot find name 'Window'.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"uri": temp_dir.uri().join("project1/file.ts").unwrap(),
|
||||
"version": 1,
|
||||
"diagnostics": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 1, "character": 0 },
|
||||
"end": { "line": 1, "character": 17 },
|
||||
},
|
||||
"severity": 1,
|
||||
"code": 2304,
|
||||
"source": "deno-ts",
|
||||
"message": "Cannot find name 'WorkerGlobalScope'.",
|
||||
},
|
||||
],
|
||||
}
|
||||
]),
|
||||
);
|
||||
client.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_deno_json_scopes_declaration_files() {
|
||||
let context = TestContextBuilder::new().use_temp_cwd().build();
|
||||
let temp_dir = context.temp_dir();
|
||||
temp_dir.create_dir_all("project1");
|
||||
temp_dir.create_dir_all("project2");
|
||||
temp_dir.write("project1/deno.json", json!({}).to_string());
|
||||
temp_dir.write("project2/deno.json", json!({}).to_string());
|
||||
temp_dir.write("project1/foo.d.ts", "declare type Foo = number;\n");
|
||||
temp_dir.write("project2/bar.d.ts", "declare type Bar = number;\n");
|
||||
let mut client = context.new_lsp_command().build();
|
||||
client.initialize_default();
|
||||
client.did_open(json!({
|
||||
"textDocument": {
|
||||
"uri": temp_dir.uri().join("project1/file.ts").unwrap(),
|
||||
"languageId": "typescript",
|
||||
"version": 1,
|
||||
"text": "export const foo: Foo = 1;\nexport const bar: Bar = 1;\n",
|
||||
},
|
||||
}));
|
||||
let diagnostics = client.did_open(json!({
|
||||
"textDocument": {
|
||||
"uri": temp_dir.uri().join("project2/file.ts").unwrap(),
|
||||
"languageId": "typescript",
|
||||
"version": 1,
|
||||
"text": "export const foo: Foo = 1;\nexport const bar: Bar = 1;\n",
|
||||
},
|
||||
}));
|
||||
assert_eq!(
|
||||
json!(diagnostics.all_messages()),
|
||||
json!([
|
||||
{
|
||||
"uri": temp_dir.uri().join("project2/file.ts").unwrap(),
|
||||
"version": 1,
|
||||
"diagnostics": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 18 },
|
||||
"end": { "line": 0, "character": 21 },
|
||||
},
|
||||
"severity": 1,
|
||||
"code": 2304,
|
||||
"source": "deno-ts",
|
||||
"message": "Cannot find name 'Foo'.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"uri": temp_dir.uri().join("project1/file.ts").unwrap(),
|
||||
"version": 1,
|
||||
"diagnostics": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 1, "character": 18 },
|
||||
"end": { "line": 1, "character": 21 },
|
||||
},
|
||||
"severity": 1,
|
||||
"code": 2304,
|
||||
"source": "deno-ts",
|
||||
"message": "Cannot find name 'Bar'.",
|
||||
},
|
||||
],
|
||||
}
|
||||
]),
|
||||
);
|
||||
client.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_deno_json_scopes_find_references() {
|
||||
let context = TestContextBuilder::new().use_temp_cwd().build();
|
||||
let temp_dir = context.temp_dir();
|
||||
temp_dir.create_dir_all("project1");
|
||||
temp_dir.create_dir_all("project2");
|
||||
temp_dir.write("project1/deno.json", json!({}).to_string());
|
||||
temp_dir.write("project2/deno.json", json!({}).to_string());
|
||||
let file1 = source_file(
|
||||
temp_dir.path().join("project1/file.ts"),
|
||||
"export const foo = 1;\n",
|
||||
);
|
||||
let file2 = source_file(
|
||||
temp_dir.path().join("project2/file.ts"),
|
||||
"export { foo } from \"../project1/file.ts\";\n",
|
||||
);
|
||||
let mut client = context.new_lsp_command().build();
|
||||
client.initialize_default();
|
||||
let res = client.write_request(
|
||||
"textDocument/references",
|
||||
json!({
|
||||
"textDocument": file1.identifier(),
|
||||
"position": file1.range_of("foo").start,
|
||||
"context": {
|
||||
"includeDeclaration": true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
res,
|
||||
json!([
|
||||
{
|
||||
"uri": file1.uri(),
|
||||
"range": file1.range_of("foo"),
|
||||
},
|
||||
{
|
||||
"uri": file2.uri(),
|
||||
"range": file2.range_of("foo"),
|
||||
},
|
||||
]),
|
||||
);
|
||||
client.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_deno_json_scopes_file_rename_import_edits() {
|
||||
let context = TestContextBuilder::new().use_temp_cwd().build();
|
||||
let temp_dir = context.temp_dir();
|
||||
temp_dir.create_dir_all("project1");
|
||||
temp_dir.create_dir_all("project2");
|
||||
temp_dir.write("project1/deno.json", json!({}).to_string());
|
||||
temp_dir.write("project2/deno.json", json!({}).to_string());
|
||||
let file1 = source_file(temp_dir.path().join("project1/file.ts"), "");
|
||||
let file2 = source_file(
|
||||
temp_dir.path().join("project2/file.ts"),
|
||||
"import \"../project1/file.ts\";\n",
|
||||
);
|
||||
let mut client = context.new_lsp_command().build();
|
||||
client.initialize_default();
|
||||
let res = client.write_request(
|
||||
"workspace/willRenameFiles",
|
||||
json!({
|
||||
"files": [
|
||||
{
|
||||
"oldUri": file1.uri(),
|
||||
"newUri": file1.uri().join("file_renamed.ts").unwrap(),
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
res,
|
||||
json!({
|
||||
"documentChanges": [
|
||||
{
|
||||
"textDocument": {
|
||||
"uri": file2.uri(),
|
||||
"version": null,
|
||||
},
|
||||
"edits": [
|
||||
{
|
||||
"range": file2.range_of("../project1/file.ts"),
|
||||
"newText": "../project1/file_renamed.ts",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
client.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_deno_json_scopes_goto_implementations() {
|
||||
let context = TestContextBuilder::new().use_temp_cwd().build();
|
||||
let temp_dir = context.temp_dir();
|
||||
temp_dir.create_dir_all("project1");
|
||||
temp_dir.create_dir_all("project2");
|
||||
temp_dir.write("project1/deno.json", json!({}).to_string());
|
||||
temp_dir.write("project2/deno.json", json!({}).to_string());
|
||||
let file1 = source_file(
|
||||
temp_dir.path().join("project1/file.ts"),
|
||||
"export interface Foo {}\n",
|
||||
);
|
||||
let file2 = source_file(
|
||||
temp_dir.path().join("project2/file.ts"),
|
||||
r#"
|
||||
import type { Foo } from "../project1/file.ts";
|
||||
export class SomeFoo implements Foo {}
|
||||
"#,
|
||||
);
|
||||
let mut client = context.new_lsp_command().build();
|
||||
client.initialize_default();
|
||||
let res = client.write_request(
|
||||
"textDocument/implementation",
|
||||
json!({
|
||||
"textDocument": file1.identifier(),
|
||||
"position": file1.range_of("Foo").start,
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
res,
|
||||
json!([
|
||||
{
|
||||
"targetUri": file2.uri(),
|
||||
"targetRange": file2.range_of("export class SomeFoo implements Foo {}"),
|
||||
"targetSelectionRange": file2.range_of("SomeFoo"),
|
||||
},
|
||||
]),
|
||||
);
|
||||
client.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_deno_json_scopes_call_hierarchy() {
|
||||
let context = TestContextBuilder::new().use_temp_cwd().build();
|
||||
let temp_dir = context.temp_dir();
|
||||
temp_dir.create_dir_all("project1");
|
||||
temp_dir.create_dir_all("project2");
|
||||
temp_dir.create_dir_all("project3");
|
||||
temp_dir.write("project1/deno.json", json!({}).to_string());
|
||||
temp_dir.write("project2/deno.json", json!({}).to_string());
|
||||
temp_dir.write("project3/deno.json", json!({}).to_string());
|
||||
let file1 = source_file(
|
||||
temp_dir.path().join("project1/file.ts"),
|
||||
r#"
|
||||
export function foo() {}
|
||||
"#,
|
||||
);
|
||||
let file2 = source_file(
|
||||
temp_dir.path().join("project2/file.ts"),
|
||||
r#"
|
||||
import { foo } from "../project1/file.ts";
|
||||
export function bar() {
|
||||
foo();
|
||||
}
|
||||
"#,
|
||||
);
|
||||
let file3 = source_file(
|
||||
temp_dir.path().join("project3/file.ts"),
|
||||
r#"
|
||||
import { bar } from "../project2/file.ts";
|
||||
bar();
|
||||
"#,
|
||||
);
|
||||
let mut client = context.new_lsp_command().build();
|
||||
client.initialize_default();
|
||||
let res = client.write_request(
|
||||
"textDocument/prepareCallHierarchy",
|
||||
json!({
|
||||
"textDocument": file2.identifier(),
|
||||
"position": file2.range_of("bar").start,
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
&res,
|
||||
&json!([
|
||||
{
|
||||
"name": "bar",
|
||||
"kind": 12,
|
||||
"detail": "",
|
||||
"uri": file2.uri(),
|
||||
"range": {
|
||||
"start": { "line": 2, "character": 6 },
|
||||
"end": { "line": 4, "character": 7 },
|
||||
},
|
||||
"selectionRange": file2.range_of("bar"),
|
||||
},
|
||||
]),
|
||||
);
|
||||
let item = res.as_array().unwrap().first().unwrap();
|
||||
let res = client
|
||||
.write_request("callHierarchy/incomingCalls", json!({ "item": item }));
|
||||
assert_eq!(
|
||||
res,
|
||||
json!([
|
||||
{
|
||||
"from": {
|
||||
"name": "file.ts",
|
||||
"kind": 2,
|
||||
"detail": "project3",
|
||||
"uri": file3.uri(),
|
||||
"range": {
|
||||
"start": { "line": 1, "character": 6 },
|
||||
"end": { "line": 3, "character": 4 },
|
||||
},
|
||||
"selectionRange": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 0 },
|
||||
},
|
||||
},
|
||||
"fromRanges": [
|
||||
{
|
||||
"start": { "line": 2, "character": 6 },
|
||||
"end": { "line": 2, "character": 9 },
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
);
|
||||
let res = client
|
||||
.write_request("callHierarchy/outgoingCalls", json!({ "item": item }));
|
||||
assert_eq!(
|
||||
res,
|
||||
json!([
|
||||
{
|
||||
"to": {
|
||||
"name": "foo",
|
||||
"kind": 12,
|
||||
"detail": "",
|
||||
"uri": file1.uri(),
|
||||
"range": file1.range_of("export function foo() {}"),
|
||||
"selectionRange": file1.range_of("foo"),
|
||||
},
|
||||
"fromRanges": [
|
||||
{
|
||||
"start": { "line": 3, "character": 8 },
|
||||
"end": { "line": 3, "character": 11 },
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
);
|
||||
client.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_deno_json_scopes_rename_symbol() {
|
||||
let context = TestContextBuilder::new().use_temp_cwd().build();
|
||||
let temp_dir = context.temp_dir();
|
||||
temp_dir.create_dir_all("project1");
|
||||
temp_dir.create_dir_all("project2");
|
||||
temp_dir.write("project1/deno.json", json!({}).to_string());
|
||||
temp_dir.write("project2/deno.json", json!({}).to_string());
|
||||
let file1 = source_file(
|
||||
temp_dir.path().join("project1/file.ts"),
|
||||
"export const foo = 1;\n",
|
||||
);
|
||||
let file2 = source_file(
|
||||
temp_dir.path().join("project2/file.ts"),
|
||||
"export { foo } from \"../project1/file.ts\";\n",
|
||||
);
|
||||
let mut client = context.new_lsp_command().build();
|
||||
client.initialize_default();
|
||||
let res = client.write_request(
|
||||
"textDocument/rename",
|
||||
json!({
|
||||
"textDocument": file1.identifier(),
|
||||
"position": file1.range_of("foo").start,
|
||||
"newName": "bar",
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
res,
|
||||
json!({
|
||||
"documentChanges": [
|
||||
{
|
||||
"textDocument": {
|
||||
"uri": file1.uri(),
|
||||
"version": null,
|
||||
},
|
||||
"edits": [
|
||||
{
|
||||
"range": file1.range_of("foo"),
|
||||
"newText": "bar",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"textDocument": {
|
||||
"uri": file2.uri(),
|
||||
"version": null,
|
||||
},
|
||||
"edits": [
|
||||
{
|
||||
"range": file2.range_of("foo"),
|
||||
"newText": "bar",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
client.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_deno_json_scopes_search_symbol() {
|
||||
let context = TestContextBuilder::new().use_temp_cwd().build();
|
||||
let temp_dir = context.temp_dir();
|
||||
temp_dir.create_dir_all("project1");
|
||||
temp_dir.create_dir_all("project2");
|
||||
temp_dir.write("project1/deno.json", json!({}).to_string());
|
||||
temp_dir.write("project2/deno.json", json!({}).to_string());
|
||||
let file1 = source_file(
|
||||
temp_dir.path().join("project1/file.ts"),
|
||||
"export const someSymbol1 = 1;\n",
|
||||
);
|
||||
let file2 = source_file(
|
||||
temp_dir.path().join("project2/file.ts"),
|
||||
"export const someSymbol2 = 2;\n",
|
||||
);
|
||||
let mut client = context.new_lsp_command().build();
|
||||
client.initialize_default();
|
||||
let res =
|
||||
client.write_request("workspace/symbol", json!({ "query": "someSymbol" }));
|
||||
assert_eq!(
|
||||
res,
|
||||
json!([
|
||||
{
|
||||
"name": "someSymbol1",
|
||||
"kind": 13,
|
||||
"location": {
|
||||
"uri": file1.uri(),
|
||||
"range": file1.range_of("someSymbol1 = 1"),
|
||||
},
|
||||
"containerName": "",
|
||||
},
|
||||
{
|
||||
"name": "someSymbol2",
|
||||
"kind": 13,
|
||||
"location": {
|
||||
"uri": file2.uri(),
|
||||
"range": file2.range_of("someSymbol2 = 2"),
|
||||
},
|
||||
"containerName": "",
|
||||
},
|
||||
]),
|
||||
);
|
||||
client.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_deno_json_workspace_fmt_config() {
|
||||
let context = TestContextBuilder::new().use_temp_cwd().build();
|
||||
|
|
Loading…
Reference in a new issue