refactor(lsp): separate document and module storage (#28469)

This commit is contained in:
Nayeem Rahman 2025-03-31 22:25:27 +01:00 committed by Divy Srivastava
parent 61b4ef6815
commit 89cdd01131
20 changed files with 4619 additions and 4839 deletions

7
Cargo.lock generated
View file

@ -1591,6 +1591,7 @@ dependencies = [
"unicode-width 0.1.13",
"uuid",
"walkdir",
"weak-table",
"winapi",
"winres",
"zip",
@ -9402,6 +9403,12 @@ dependencies = [
"thiserror 2.0.12",
]
[[package]]
name = "weak-table"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "323f4da9523e9a669e1eaf9c6e763892769b1d38c623913647bfdc1532fe4549"
[[package]]
name = "web-sys"
version = "0.3.77"

View file

@ -327,6 +327,7 @@ url = { version = "2.5", features = ["serde", "expose_internals"] }
urlpattern = "0.3.0"
uuid = { version = "1.3.0", features = ["v4"] }
walkdir = "=2.5.0"
weak-table = "0.3.2"
web-transport-proto = "0.2.3"
webpki-root-certs = "0.26.5"
webpki-roots = "0.26"

View file

@ -177,6 +177,7 @@ typed-arena.workspace = true
unicode-width.workspace = true
uuid = { workspace = true, features = ["serde"] }
walkdir.workspace = true
weak-table.workspace = true
zip = { workspace = true, features = ["deflate-flate2"] }
zstd.workspace = true

View file

@ -130,11 +130,27 @@ fn bench_deco_apps_edits(deno_exe: &Path) -> Duration {
}
}),
);
let re = lazy_regex::regex!(r"Documents in memory: (\d+)");
let open_re = lazy_regex::regex!(r"Open: (\d+)");
let server_re = lazy_regex::regex!(r"Server: (\d+)");
let res = res.as_str().unwrap().to_string();
assert!(res.starts_with("# Deno Language Server Status"));
let captures = re.captures(&res).unwrap();
let count = captures.get(1).unwrap().as_str().parse::<usize>().unwrap();
let open_count = open_re
.captures(&res)
.unwrap()
.get(1)
.unwrap()
.as_str()
.parse::<usize>()
.unwrap();
let server_count = server_re
.captures(&res)
.unwrap()
.get(1)
.unwrap()
.as_str()
.parse::<usize>()
.unwrap();
let count = open_count + server_count;
assert!(count > 1000, "count: {}", count);
client.shutdown();

View file

@ -5,12 +5,14 @@ use std::cmp::Ordering;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::Path;
use std::sync::Arc;
use deno_ast::SourceRange;
use deno_ast::SourceRangedForSpanned;
use deno_ast::SourceTextInfo;
use deno_core::anyhow::anyhow;
use deno_core::error::AnyError;
use deno_core::resolve_url;
use deno_core::serde::Deserialize;
use deno_core::serde::Serialize;
use deno_core::serde_json;
@ -33,6 +35,7 @@ use deno_semver::SmallStackString;
use deno_semver::StackString;
use deno_semver::Version;
use import_map::ImportMap;
use lsp_types::Uri;
use node_resolver::InNpmPackageChecker;
use node_resolver::NodeResolutionKind;
use node_resolver::ResolutionMode;
@ -45,13 +48,12 @@ use tower_lsp::lsp_types::Range;
use super::diagnostics::DenoDiagnostic;
use super::diagnostics::DiagnosticSource;
use super::documents::Documents;
use super::documents::DocumentModule;
use super::documents::DocumentModules;
use super::language_server;
use super::resolver::LspResolver;
use super::tsc;
use super::urls::url_to_uri;
use crate::args::jsr_url;
use crate::lsp::logging::lsp_warn;
use crate::tools::lint::CliLinter;
use crate::util::path::relative_specifier;
@ -233,27 +235,27 @@ fn code_as_string(code: &Option<lsp::NumberOrString>) -> String {
/// Rewrites imports in quick fixes and code changes to be Deno specific.
pub struct TsResponseImportMapper<'a> {
documents: &'a Documents,
document_modules: &'a DocumentModules,
scope: Option<Arc<ModuleSpecifier>>,
maybe_import_map: Option<&'a ImportMap>,
resolver: &'a LspResolver,
tsc_specifier_map: &'a tsc::TscSpecifierMap,
file_referrer: ModuleSpecifier,
}
impl<'a> TsResponseImportMapper<'a> {
pub fn new(
documents: &'a Documents,
document_modules: &'a DocumentModules,
scope: Option<Arc<ModuleSpecifier>>,
maybe_import_map: Option<&'a ImportMap>,
resolver: &'a LspResolver,
tsc_specifier_map: &'a tsc::TscSpecifierMap,
file_referrer: &ModuleSpecifier,
) -> Self {
Self {
documents,
document_modules,
scope,
maybe_import_map,
resolver,
tsc_specifier_map,
file_referrer: file_referrer.clone(),
}
}
@ -299,7 +301,7 @@ impl<'a> TsResponseImportMapper<'a> {
let export = self.resolver.jsr_lookup_export_for_path(
&nv,
&path,
Some(&self.file_referrer),
self.scope.as_deref(),
)?;
let sub_path = (export != ".")
.then_some(export)
@ -327,7 +329,7 @@ impl<'a> TsResponseImportMapper<'a> {
req = req.or_else(|| {
self
.resolver
.jsr_lookup_req_for_nv(&nv, Some(&self.file_referrer))
.jsr_lookup_req_for_nv(&nv, self.scope.as_deref())
});
let spec_str = if let Some(req) = req {
let req_ref = PackageReqReference { req, sub_path };
@ -357,11 +359,11 @@ impl<'a> TsResponseImportMapper<'a> {
if let Some(npm_resolver) = self
.resolver
.maybe_managed_npm_resolver(Some(&self.file_referrer))
.maybe_managed_npm_resolver(self.scope.as_deref())
{
let in_npm_pkg = self
.resolver
.in_npm_pkg_checker(Some(&self.file_referrer))
.in_npm_pkg_checker(self.scope.as_deref())
.in_npm_package(specifier);
if in_npm_pkg {
if let Ok(Some(pkg_id)) =
@ -428,7 +430,7 @@ impl<'a> TsResponseImportMapper<'a> {
}
} else if let Some(dep_name) = self
.resolver
.file_url_to_package_json_dep(specifier, Some(&self.file_referrer))
.file_url_to_package_json_dep(specifier, self.scope.as_deref())
{
return Some(dep_name);
}
@ -515,7 +517,7 @@ impl<'a> TsResponseImportMapper<'a> {
for specifier in specifiers {
if let Some(specifier) = self
.resolver
.as_cli_resolver(Some(&self.file_referrer))
.as_cli_resolver(self.scope.as_deref())
.resolve(
&specifier,
referrer,
@ -525,7 +527,11 @@ impl<'a> TsResponseImportMapper<'a> {
)
.ok()
.and_then(|s| self.tsc_specifier_map.normalize(s.as_str()).ok())
.filter(|s| self.documents.exists(s, Some(&self.file_referrer)))
.filter(|s| {
self
.document_modules
.specifier_exists(s, self.scope.as_deref())
})
{
if let Some(specifier) = self
.check_specifier(&specifier, referrer)
@ -547,7 +553,7 @@ impl<'a> TsResponseImportMapper<'a> {
) -> bool {
self
.resolver
.as_cli_resolver(Some(&self.file_referrer))
.as_cli_resolver(self.scope.as_deref())
.resolve(
specifier_text,
referrer,
@ -645,6 +651,7 @@ fn try_reverse_map_package_json_exports(
/// like an import and rewrite the import specifier to include the extension
pub fn fix_ts_import_changes(
changes: &[tsc::FileTextChanges],
module: &DocumentModule,
language_server: &language_server::Inner,
token: &CancellationToken,
) -> Result<Vec<tsc::FileTextChanges>, AnyError> {
@ -653,16 +660,29 @@ pub fn fix_ts_import_changes(
if token.is_cancelled() {
return Err(anyhow!("request cancelled"));
}
let Ok(referrer) = ModuleSpecifier::parse(&change.file_name) else {
let is_new_file = change.is_new_file.unwrap_or(false);
let Ok(target_specifier) = resolve_url(&change.file_name) else {
continue;
};
let referrer_doc = language_server.get_asset_or_document(&referrer).ok();
let resolution_mode = referrer_doc
let target_module = if is_new_file {
None
} else {
let Some(target_module) = language_server
.document_modules
.inspect_module_for_specifier(
&target_specifier,
module.scope.as_deref(),
)
else {
continue;
};
Some(target_module)
};
let resolution_mode = target_module
.as_ref()
.map(|d| d.resolution_mode())
.map(|m| m.resolution_mode)
.unwrap_or(ResolutionMode::Import);
let import_mapper =
language_server.get_ts_response_import_mapper(&referrer);
let import_mapper = language_server.get_ts_response_import_mapper(module);
let mut text_changes = Vec::new();
for text_change in &change.text_changes {
let lines = text_change.new_text.split('\n');
@ -673,7 +693,11 @@ pub fn fix_ts_import_changes(
let specifier =
captures.iter().skip(1).find_map(|s| s).unwrap().as_str();
if let Some(new_specifier) = import_mapper
.check_unresolved_specifier(specifier, &referrer, resolution_mode)
.check_unresolved_specifier(
specifier,
&target_specifier,
resolution_mode,
)
{
line.replace(specifier, &new_specifier)
} else {
@ -702,9 +726,8 @@ pub fn fix_ts_import_changes(
/// Fix tsc import code actions so that the module specifier is correct for
/// resolution by Deno (includes the extension).
fn fix_ts_import_action<'a>(
referrer: &ModuleSpecifier,
resolution_mode: ResolutionMode,
action: &'a tsc::CodeFixAction,
module: &DocumentModule,
language_server: &language_server::Inner,
) -> Option<Cow<'a, tsc::CodeFixAction>> {
if !matches!(
@ -721,11 +744,11 @@ fn fix_ts_import_action<'a>(
let Some(specifier) = specifier else {
return Some(Cow::Borrowed(action));
};
let import_mapper = language_server.get_ts_response_import_mapper(referrer);
let import_mapper = language_server.get_ts_response_import_mapper(module);
if let Some(new_specifier) = import_mapper.check_unresolved_specifier(
specifier,
referrer,
resolution_mode,
&module.specifier,
module.resolution_mode,
) {
let description = action.description.replace(specifier, &new_specifier);
let changes = action
@ -756,8 +779,11 @@ fn fix_ts_import_action<'a>(
fix_id: None,
fix_all_description: None,
}))
} else if !import_mapper.is_valid_import(specifier, referrer, resolution_mode)
{
} else if !import_mapper.is_valid_import(
specifier,
&module.specifier,
module.resolution_mode,
) {
None
} else {
Some(Cow::Borrowed(action))
@ -818,16 +844,14 @@ fn is_preferred(
/// for an LSP CodeAction.
pub fn ts_changes_to_edit(
changes: &[tsc::FileTextChanges],
module: &DocumentModule,
language_server: &language_server::Inner,
) -> Result<Option<lsp::WorkspaceEdit>, AnyError> {
let mut text_document_edits = Vec::new();
for change in changes {
let edit = match change.to_text_document_edit(language_server) {
Ok(e) => e,
Err(err) => {
lsp_warn!("Couldn't covert text document edit: {:#}", err);
continue;
}
let Some(edit) = change.to_text_document_edit(module, language_server)
else {
continue;
};
text_document_edits.push(edit);
}
@ -841,7 +865,7 @@ pub fn ts_changes_to_edit(
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeActionData {
pub specifier: ModuleSpecifier,
pub uri: Uri,
pub fix_id: String,
}
@ -866,27 +890,27 @@ pub struct CodeActionCollection {
impl CodeActionCollection {
pub fn add_deno_fix_action(
&mut self,
uri: &Uri,
specifier: &ModuleSpecifier,
diagnostic: &lsp::Diagnostic,
) -> Result<(), AnyError> {
let code_action = DenoDiagnostic::get_code_action(specifier, diagnostic)?;
let code_action =
DenoDiagnostic::get_code_action(uri, specifier, diagnostic)?;
self.actions.push(CodeActionKind::Deno(code_action));
Ok(())
}
pub fn add_deno_lint_actions(
&mut self,
specifier: &ModuleSpecifier,
uri: &Uri,
module: &DocumentModule,
diagnostic: &lsp::Diagnostic,
maybe_text_info: Option<&SourceTextInfo>,
maybe_parsed_source: Option<&deno_ast::ParsedSource>,
) -> Result<(), AnyError> {
if let Some(data_quick_fixes) = diagnostic
.data
.as_ref()
.and_then(|d| serde_json::from_value::<Vec<DataQuickFix>>(d.clone()).ok())
{
let uri = url_to_uri(specifier)?;
for quick_fix in data_quick_fixes {
let mut changes = HashMap::new();
changes.insert(
@ -917,22 +941,15 @@ impl CodeActionCollection {
self.actions.push(CodeActionKind::DenoLint(code_action));
}
}
self.add_deno_lint_ignore_action(
specifier,
diagnostic,
maybe_text_info,
maybe_parsed_source,
)
self.add_deno_lint_ignore_action(uri, module, diagnostic)
}
fn add_deno_lint_ignore_action(
&mut self,
specifier: &ModuleSpecifier,
uri: &Uri,
module: &DocumentModule,
diagnostic: &lsp::Diagnostic,
maybe_text_info: Option<&SourceTextInfo>,
maybe_parsed_source: Option<&deno_ast::ParsedSource>,
) -> Result<(), AnyError> {
let uri = url_to_uri(specifier)?;
let code = diagnostic
.code
.as_ref()
@ -941,11 +958,11 @@ impl CodeActionCollection {
_ => "".to_string(),
})
.unwrap();
let text_info = module.text_info();
let line_content = maybe_text_info.map(|ti| {
ti.line_text(diagnostic.range.start.line as usize)
.to_string()
});
let line_content = text_info
.line_text(diagnostic.range.start.line as usize)
.to_string();
let mut changes = HashMap::new();
changes.insert(
@ -953,7 +970,7 @@ impl CodeActionCollection {
vec![lsp::TextEdit {
new_text: prepend_whitespace(
format!("// deno-lint-ignore {code}\n"),
line_content,
Some(line_content),
),
range: lsp::Range {
start: lsp::Position {
@ -986,20 +1003,25 @@ impl CodeActionCollection {
.push(CodeActionKind::DenoLint(ignore_error_action));
// Disable a lint error for the entire file.
let maybe_ignore_comment = maybe_parsed_source.and_then(|ps| {
// Note: we can use ps.get_leading_comments() but it doesn't
// work when shebang is present at the top of the file.
ps.comments().get_vec().iter().find_map(|c| {
let comment_text = c.text.trim();
comment_text.split_whitespace().next().and_then(|prefix| {
if prefix == "deno-lint-ignore-file" {
Some(c.clone())
} else {
None
}
let maybe_ignore_comment = module
.open_data
.as_ref()
.and_then(|d| d.parsed_source.as_ref())
.and_then(|ps| {
let ps = ps.as_ref().ok()?;
// Note: we can use ps.get_leading_comments() but it doesn't
// work when shebang is present at the top of the file.
ps.comments().get_vec().iter().find_map(|c| {
let comment_text = c.text.trim();
comment_text.split_whitespace().next().and_then(|prefix| {
if prefix == "deno-lint-ignore-file" {
Some(c.clone())
} else {
None
}
})
})
})
});
});
let mut new_text = format!("// deno-lint-ignore-file {code}\n");
let mut range = lsp::Range {
@ -1017,9 +1039,7 @@ impl CodeActionCollection {
if let Some(ignore_comment) = maybe_ignore_comment {
new_text = format!(" {code}");
// Get the end position of the comment.
let line = maybe_text_info
.unwrap()
.line_and_column_index(ignore_comment.end());
let line = text_info.line_and_column_index(ignore_comment.end());
let position = lsp::Position {
line: line.line_index as u32,
character: line.column_index as u32,
@ -1051,7 +1071,7 @@ impl CodeActionCollection {
let mut changes = HashMap::new();
changes.insert(
uri,
uri.clone(),
vec![lsp::TextEdit {
new_text: "// deno-lint-ignore-file\n".to_string(),
range: lsp::Range {
@ -1090,10 +1110,9 @@ impl CodeActionCollection {
/// Add a TypeScript code fix action to the code actions collection.
pub fn add_ts_fix_action(
&mut self,
specifier: &ModuleSpecifier,
resolution_mode: ResolutionMode,
action: &tsc::CodeFixAction,
diagnostic: &lsp::Diagnostic,
module: &DocumentModule,
language_server: &language_server::Inner,
) -> Result<(), AnyError> {
if action.commands.is_some() {
@ -1112,12 +1131,11 @@ impl CodeActionCollection {
.into(),
);
}
let Some(action) =
fix_ts_import_action(specifier, resolution_mode, action, language_server)
let Some(action) = fix_ts_import_action(action, module, language_server)
else {
return Ok(());
};
let edit = ts_changes_to_edit(&action.changes, language_server)?;
let edit = ts_changes_to_edit(&action.changes, module, language_server)?;
let code_action = lsp::CodeAction {
title: action.description.clone(),
kind: Some(lsp::CodeActionKind::QUICKFIX),
@ -1160,12 +1178,12 @@ impl CodeActionCollection {
pub fn add_ts_fix_all_action(
&mut self,
action: &tsc::CodeFixAction,
specifier: &ModuleSpecifier,
module: &DocumentModule,
diagnostic: &lsp::Diagnostic,
) {
let data = action.fix_id.as_ref().map(|fix_id| {
json!(CodeActionData {
specifier: specifier.clone(),
uri: module.uri.as_ref().clone(),
fix_id: fix_id.clone(),
})
});

View file

@ -19,22 +19,8 @@ use crate::lsp::logging::lsp_log;
use crate::lsp::logging::lsp_warn;
use crate::sys::CliSys;
pub fn calculate_fs_version(
cache: &LspCache,
specifier: &ModuleSpecifier,
file_referrer: Option<&ModuleSpecifier>,
) -> Option<String> {
match specifier.scheme() {
"npm" | "node" | "data" | "blob" => None,
"file" => url_to_file_path(specifier)
.ok()
.and_then(|path| calculate_fs_version_at_path(&path)),
_ => calculate_fs_version_in_cache(cache, specifier, file_referrer),
}
}
/// Calculate a version for for a given path.
pub fn calculate_fs_version_at_path(path: &Path) -> Option<String> {
pub fn calculate_fs_version_at_path(path: impl AsRef<Path>) -> Option<String> {
let metadata = fs::metadata(path).ok()?;
if let Ok(modified) = metadata.modified() {
if let Ok(n) = modified.duration_since(SystemTime::UNIX_EPOCH) {
@ -47,32 +33,11 @@ pub fn calculate_fs_version_at_path(path: &Path) -> Option<String> {
}
}
fn calculate_fs_version_in_cache(
cache: &LspCache,
specifier: &ModuleSpecifier,
file_referrer: Option<&ModuleSpecifier>,
) -> Option<String> {
let http_cache = cache.for_specifier(file_referrer);
let Ok(cache_key) = http_cache.cache_item_key(specifier) else {
return Some("1".to_string());
};
match http_cache.read_modified_time(&cache_key) {
Ok(Some(modified)) => {
match modified.duration_since(SystemTime::UNIX_EPOCH) {
Ok(n) => Some(n.as_millis().to_string()),
Err(_) => Some("1".to_string()),
}
}
Ok(None) => None,
Err(_) => Some("1".to_string()),
}
}
#[derive(Debug, Clone)]
pub struct LspCache {
deno_dir: DenoDir,
global: Arc<GlobalHttpCache>,
vendors_by_scope: BTreeMap<ModuleSpecifier, Option<Arc<LocalLspHttpCache>>>,
vendors_by_scope: BTreeMap<Arc<Url>, Option<Arc<LocalLspHttpCache>>>,
}
impl Default for LspCache {
@ -178,11 +143,30 @@ impl LspCache {
vendor.get_remote_url(&path)
}
pub fn is_valid_file_referrer(&self, specifier: &ModuleSpecifier) -> bool {
if let Ok(path) = url_to_file_path(specifier) {
if !path.starts_with(&self.deno_dir().root) {
return true;
}
pub fn in_cache_directory(&self, specifier: &Url) -> bool {
let Ok(path) = url_to_file_path(specifier) else {
return false;
};
if path.starts_with(&self.deno_dir().root) {
return true;
}
let Some(vendor) = self
.vendors_by_scope
.iter()
.rfind(|(s, _)| specifier.as_str().starts_with(s.as_str()))
.and_then(|(_, c)| c.as_ref())
else {
return false;
};
vendor.get_remote_url(&path).is_some()
}
pub fn in_global_cache_directory(&self, specifier: &Url) -> bool {
let Ok(path) = url_to_file_path(specifier) else {
return false;
};
if path.starts_with(&self.deno_dir().root) {
return true;
}
false
}

View file

@ -11,18 +11,19 @@ use deno_ast::swc::ecma_visit::VisitWith;
use deno_ast::ParsedSource;
use deno_ast::SourceRange;
use deno_ast::SourceRangedForSpanned;
use deno_core::anyhow::anyhow;
use deno_core::error::AnyError;
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::ModuleSpecifier;
use lazy_regex::lazy_regex;
use lsp_types::Uri;
use once_cell::sync::Lazy;
use regex::Regex;
use tokio_util::sync::CancellationToken;
use tower_lsp::jsonrpc::Error as LspError;
use tower_lsp::jsonrpc::Result as LspResult;
use tower_lsp::lsp_types as lsp;
use super::analysis::source_range_to_lsp_range;
@ -36,7 +37,7 @@ static ABSTRACT_MODIFIER: Lazy<Regex> = lazy_regex!(r"\babstract\b");
static EXPORT_MODIFIER: Lazy<Regex> = lazy_regex!(r"\bexport\b");
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Deserialize, Serialize)]
pub enum CodeLensSource {
#[serde(rename = "implementations")]
Implementations,
@ -44,11 +45,11 @@ pub enum CodeLensSource {
References,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeLensData {
pub source: CodeLensSource,
pub specifier: ModuleSpecifier,
pub uri: Uri,
}
struct DenoTestCollector {
@ -254,83 +255,61 @@ async fn resolve_implementation_code_lens(
data: CodeLensData,
language_server: &language_server::Inner,
token: &CancellationToken,
) -> Result<lsp::CodeLens, AnyError> {
let asset_or_doc = language_server.get_asset_or_document(&data.specifier)?;
let line_index = asset_or_doc.line_index();
let maybe_implementations = language_server
.ts_server
.get_implementations(
language_server.snapshot(),
data.specifier.clone(),
line_index.offset_tsc(code_lens.range.start)?,
) -> LspResult<lsp::CodeLens> {
let locations = language_server
.goto_implementation(
lsp::request::GotoImplementationParams {
text_document_position_params: lsp::TextDocumentPositionParams {
text_document: lsp::TextDocumentIdentifier {
uri: data.uri.clone(),
},
position: code_lens.range.start,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
},
token,
)
.await
.map_err(|err| {
if token.is_cancelled() {
anyhow!("request cancelled")
} else {
anyhow!(
"Unable to get implementation locations from TypeScript: {:#}",
err
)
}
})?;
if let Some(implementations) = maybe_implementations {
let mut locations = Vec::new();
for implementation in implementations {
if token.is_cancelled() {
break;
}
let implementation_specifier =
resolve_url(&implementation.document_span.file_name)?;
let implementation_location =
implementation.to_location(line_index.clone(), language_server);
if !(implementation_specifier == data.specifier
&& implementation_location.range.start == code_lens.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()
};
lsp::Command {
title,
command: "deno.client.showReferences".to_string(),
arguments: Some(vec![
json!(data.specifier),
json!(code_lens.range.start),
json!(locations),
]),
}
} else {
lsp::Command {
title: "0 implementations".to_string(),
command: "".to_string(),
arguments: None,
}
};
Ok(lsp::CodeLens {
range: code_lens.range,
command: Some(command),
data: None,
.await?
.map(|r| match r {
lsp::GotoDefinitionResponse::Scalar(location) => vec![location],
lsp::GotoDefinitionResponse::Array(locations) => locations,
lsp::GotoDefinitionResponse::Link(links) => links
.into_iter()
.map(|l| lsp::Location {
uri: l.target_uri,
range: l.target_selection_range,
})
.collect(),
})
.unwrap_or(Vec::new());
let title = if locations.len() == 1 {
"1 implementation".to_string()
} else {
let command = Some(lsp::Command {
title: "0 implementations".to_string(),
command: "".to_string(),
format!("{} implementations", locations.len())
};
let command = if locations.is_empty() {
lsp::Command {
title,
command: String::new(),
arguments: None,
});
Ok(lsp::CodeLens {
range: code_lens.range,
command,
data: None,
})
}
}
} else {
lsp::Command {
title,
command: "deno.client.showReferences".to_string(),
arguments: Some(vec![
json!(data.uri),
json!(code_lens.range.start),
json!(locations),
]),
}
};
Ok(lsp::CodeLens {
range: code_lens.range,
command: Some(command),
data: None,
})
}
async fn resolve_references_code_lens(
@ -338,59 +317,26 @@ async fn resolve_references_code_lens(
data: CodeLensData,
language_server: &language_server::Inner,
token: &CancellationToken,
) -> Result<lsp::CodeLens, AnyError> {
fn get_locations(
maybe_referenced_symbols: Option<Vec<tsc::ReferencedSymbol>>,
language_server: &language_server::Inner,
token: &CancellationToken,
) -> Result<Vec<lsp::Location>, AnyError> {
let symbols = match maybe_referenced_symbols {
Some(symbols) => symbols,
None => return Ok(Vec::new()),
};
let mut locations = Vec::new();
for reference in symbols.iter().flat_map(|s| &s.references) {
if token.is_cancelled() {
break;
}
if reference.is_definition {
continue;
}
let reference_specifier =
resolve_url(&reference.entry.document_span.file_name)?;
let asset_or_doc =
language_server.get_asset_or_document(&reference_specifier)?;
locations.push(
reference
.entry
.to_location(asset_or_doc.line_index(), language_server),
);
}
Ok(locations)
}
let asset_or_document =
language_server.get_asset_or_document(&data.specifier)?;
let line_index = asset_or_document.line_index();
let maybe_referenced_symbols = language_server
.ts_server
.find_references(
language_server.snapshot(),
data.specifier.clone(),
line_index.offset_tsc(code_lens.range.start)?,
) -> LspResult<lsp::CodeLens> {
let locations = language_server
.references(
lsp::ReferenceParams {
text_document_position: lsp::TextDocumentPositionParams {
text_document: lsp::TextDocumentIdentifier {
uri: data.uri.clone(),
},
position: code_lens.range.start,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
context: lsp::ReferenceContext {
include_declaration: false,
},
},
token,
)
.await
.map_err(|err| {
if token.is_cancelled() {
anyhow!("request cancelled")
} else {
anyhow!("Unable to get references from TypeScript: {:#}", err)
}
})?;
let locations =
get_locations(maybe_referenced_symbols, language_server, token)?;
.await?
.unwrap_or_default();
let title = if locations.len() == 1 {
"1 reference".to_string()
} else {
@ -407,7 +353,7 @@ async fn resolve_references_code_lens(
title,
command: "deno.client.showReferences".to_string(),
arguments: Some(vec![
json!(data.specifier),
json!(data.uri),
json!(code_lens.range.start),
json!(locations),
]),
@ -424,9 +370,14 @@ pub async fn resolve_code_lens(
code_lens: lsp::CodeLens,
language_server: &language_server::Inner,
token: &CancellationToken,
) -> Result<lsp::CodeLens, AnyError> {
) -> LspResult<lsp::CodeLens> {
let data: CodeLensData =
serde_json::from_value(code_lens.data.clone().unwrap())?;
serde_json::from_value(code_lens.data.clone().unwrap()).map_err(|err| {
LspError::invalid_params(format!(
"Unable to parse code lens data: {:#}",
err
))
})?;
match data.source {
CodeLensSource::Implementations => {
resolve_implementation_code_lens(code_lens, data, language_server, token)
@ -453,7 +404,7 @@ pub fn collect_test(
/// Return tsc navigation tree code lenses.
pub fn collect_tsc(
specifier: &ModuleSpecifier,
uri: &Uri,
code_lens_settings: &CodeLensSettings,
line_index: Arc<LineIndex>,
navigation_tree: &NavigationTree,
@ -468,11 +419,7 @@ pub fn collect_tsc(
let source = CodeLensSource::Implementations;
match i.kind {
tsc::ScriptElementKind::InterfaceElement => {
code_lenses.push(i.to_code_lens(
line_index.clone(),
specifier,
&source,
));
code_lenses.push(i.to_code_lens(line_index.clone(), uri, source));
}
tsc::ScriptElementKind::ClassElement
| tsc::ScriptElementKind::MemberFunctionElement
@ -480,11 +427,7 @@ pub fn collect_tsc(
| tsc::ScriptElementKind::MemberGetAccessorElement
| tsc::ScriptElementKind::MemberSetAccessorElement => {
if ABSTRACT_MODIFIER.is_match(&i.kind_modifiers) {
code_lenses.push(i.to_code_lens(
line_index.clone(),
specifier,
&source,
));
code_lenses.push(i.to_code_lens(line_index.clone(), uri, source));
}
}
_ => (),
@ -496,51 +439,31 @@ pub fn collect_tsc(
let source = CodeLensSource::References;
if let Some(parent) = &mp {
if parent.kind == tsc::ScriptElementKind::EnumElement {
code_lenses.push(i.to_code_lens(
line_index.clone(),
specifier,
&source,
));
code_lenses.push(i.to_code_lens(line_index.clone(), uri, source));
}
}
match i.kind {
tsc::ScriptElementKind::FunctionElement => {
if code_lens_settings.references_all_functions {
code_lenses.push(i.to_code_lens(
line_index.clone(),
specifier,
&source,
));
code_lenses.push(i.to_code_lens(line_index.clone(), uri, 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.clone(),
specifier,
&source,
));
code_lenses.push(i.to_code_lens(line_index.clone(), uri, source));
}
}
tsc::ScriptElementKind::ClassElement => {
if i.text != "<class>" {
code_lenses.push(i.to_code_lens(
line_index.clone(),
specifier,
&source,
));
code_lenses.push(i.to_code_lens(line_index.clone(), uri, source));
}
}
tsc::ScriptElementKind::InterfaceElement
| tsc::ScriptElementKind::TypeElement
| tsc::ScriptElementKind::EnumElement => {
code_lenses.push(i.to_code_lens(
line_index.clone(),
specifier,
&source,
));
code_lenses.push(i.to_code_lens(line_index.clone(), uri, source));
}
tsc::ScriptElementKind::LocalFunctionElement
| tsc::ScriptElementKind::MemberFunctionElement
@ -556,8 +479,8 @@ pub fn collect_tsc(
| tsc::ScriptElementKind::TypeElement => {
code_lenses.push(i.to_code_lens(
line_index.clone(),
specifier,
&source,
uri,
source,
));
}
_ => (),
@ -575,6 +498,7 @@ pub fn collect_tsc(
#[cfg(test)]
mod tests {
use deno_ast::MediaType;
use deno_core::resolve_url;
use super::*;

View file

@ -25,8 +25,9 @@ use tower_lsp::lsp_types as lsp;
use super::client::Client;
use super::config::Config;
use super::config::WorkspaceSettings;
use super::documents::Documents;
use super::documents::DocumentsFilter;
use super::documents::DocumentModule;
use super::documents::DocumentModules;
use super::documents::ServerDocumentKind;
use super::jsr::CliJsrSearchApi;
use super::lsp_custom;
use super::npm::CliNpmSearchApi;
@ -152,38 +153,36 @@ fn to_narrow_lsp_range(
#[allow(clippy::too_many_arguments)]
#[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
pub async fn get_import_completions(
specifier: &ModuleSpecifier,
module: &DocumentModule,
position: &lsp::Position,
config: &Config,
client: &Client,
module_registries: &ModuleRegistry,
jsr_search_api: &CliJsrSearchApi,
npm_search_api: &CliNpmSearchApi,
documents: &Documents,
document_modules: &DocumentModules,
resolver: &LspResolver,
maybe_import_map: Option<&ImportMap>,
) -> Option<lsp::CompletionResponse> {
let document = documents.get(specifier)?;
let file_referrer = document.file_referrer();
let (text, _, graph_range) = document.get_maybe_dependency(position)?;
let (text, _, graph_range) = module.dependency_at_position(position)?;
let resolution_mode = graph_range
.resolution_mode
.map(to_node_resolution_mode)
.unwrap_or_else(|| document.resolution_mode());
let range = to_narrow_lsp_range(document.text_info(), graph_range.range);
.unwrap_or_else(|| module.resolution_mode);
let range = to_narrow_lsp_range(module.text_info(), graph_range.range);
let resolved = resolver
.as_cli_resolver(file_referrer)
.as_cli_resolver(module.scope.as_deref())
.resolve(
&text,
specifier,
text,
&module.specifier,
deno_graph::Position::zeroed(),
resolution_mode,
NodeResolutionKind::Execution,
)
.ok();
if let Some(completion_list) = get_jsr_completions(
specifier,
&text,
&module.specifier,
text,
&range,
resolved.as_ref(),
jsr_search_api,
@ -193,39 +192,46 @@ pub async fn get_import_completions(
{
Some(lsp::CompletionResponse::List(completion_list))
} else if let Some(completion_list) =
get_npm_completions(specifier, &text, &range, npm_search_api).await
get_npm_completions(&module.specifier, text, &range, npm_search_api).await
{
Some(lsp::CompletionResponse::List(completion_list))
} else if let Some(completion_list) = get_node_completions(&text, &range) {
} else if let Some(completion_list) = get_node_completions(text, &range) {
Some(lsp::CompletionResponse::List(completion_list))
} else if let Some(completion_list) =
get_import_map_completions(specifier, &text, &range, maybe_import_map)
{
} else if let Some(completion_list) = get_import_map_completions(
&module.specifier,
text,
&range,
maybe_import_map,
) {
// completions for import map specifiers
Some(lsp::CompletionResponse::List(completion_list))
} else if let Some(completion_list) =
get_local_completions(specifier, resolution_mode, &text, &range, resolver)
{
} else if let Some(completion_list) = get_local_completions(
&module.specifier,
resolution_mode,
text,
&range,
resolver,
) {
// completions for local relative modules
Some(lsp::CompletionResponse::List(completion_list))
} else if !text.is_empty() {
// completion of modules from a module registry or cache
check_auto_config_registry(
&text,
config.workspace_settings_for_specifier(specifier),
text,
config.workspace_settings_for_specifier(&module.specifier),
client,
module_registries,
)
.await;
let maybe_list = module_registries
.get_completions(&text, &range, resolved.as_ref(), |s| {
documents.exists(s, file_referrer)
.get_completions(text, &range, resolved.as_ref(), |s| {
document_modules.specifier_exists(s, module.scope.as_deref())
})
.await;
let maybe_list = maybe_list
.or_else(|| module_registries.get_origin_completions(&text, &range));
.or_else(|| module_registries.get_origin_completions(text, &range));
let list = maybe_list.unwrap_or_else(|| CompletionList {
items: get_workspace_completions(specifier, &text, &range, documents),
items: get_remote_completions(module, text, &range, document_modules),
is_incomplete: false,
});
Some(lsp::CompletionResponse::List(list))
@ -248,10 +254,13 @@ pub async fn get_import_completions(
.collect();
let mut is_incomplete = false;
if let Some(import_map) = maybe_import_map {
items.extend(get_base_import_map_completions(import_map, specifier));
items.extend(get_base_import_map_completions(
import_map,
&module.specifier,
));
}
if let Some(origin_items) =
module_registries.get_origin_completions(&text, &range)
module_registries.get_origin_completions(text, &range)
{
is_incomplete = origin_items.is_incomplete;
items.extend(origin_items.items);
@ -440,22 +449,6 @@ fn get_local_completions(
}
}
fn get_relative_specifiers(
base: &ModuleSpecifier,
specifiers: Vec<ModuleSpecifier>,
) -> Vec<String> {
specifiers
.iter()
.filter_map(|s| {
if s != base {
Some(relative_specifier(base, s).unwrap_or_else(|| s.to_string()))
} else {
None
}
})
.collect()
}
/// Find the index of the '@' delimiting the package name and version, if any.
fn parse_bare_specifier_version_index(bare_specifier: &str) -> Option<usize> {
if bare_specifier.starts_with('@') {
@ -770,34 +763,33 @@ fn get_node_completions(
})
}
/// Get workspace completions that include modules in the Deno cache which match
/// Get remote completions that include modules in the Deno cache which match
/// the current specifier string.
fn get_workspace_completions(
specifier: &ModuleSpecifier,
fn get_remote_completions(
module: &DocumentModule,
current: &str,
range: &lsp::Range,
documents: &Documents,
document_modules: &DocumentModules,
) -> Vec<lsp::CompletionItem> {
let workspace_specifiers = documents
.documents(DocumentsFilter::AllDiagnosable)
.into_iter()
.map(|d| d.specifier().clone())
.collect();
let specifier_strings =
get_relative_specifiers(specifier, workspace_specifiers);
specifier_strings
let specifiers = document_modules
.documents
.server_docs()
.into_iter()
.filter_map(|d| {
if let ServerDocumentKind::RemoteUrl { url, .. } = &d.kind {
if *url == module.specifier {
return None;
}
return Some(
relative_specifier(&module.specifier, url)
.unwrap_or_else(|| url.to_string()),
);
}
None
});
specifiers
.filter_map(|label| {
if label.starts_with(current) {
let detail = Some(
if label.starts_with("http:") || label.starts_with("https:") {
"(remote)".to_string()
} else if label.starts_with("data:") {
"(data)".to_string()
} else {
"(local)".to_string()
},
);
let text_edit = Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: *range,
new_text: label.clone(),
@ -805,7 +797,7 @@ fn get_workspace_completions(
Some(lsp::CompletionItem {
label,
kind: Some(lsp::CompletionItemKind::FILE),
detail,
detail: Some("(remote)".to_string()),
sort_text: Some("1".to_string()),
text_edit,
commit_characters: Some(
@ -831,18 +823,18 @@ mod tests {
use super::*;
use crate::cache::HttpCache;
use crate::lsp::cache::LspCache;
use crate::lsp::documents::Documents;
use crate::lsp::documents::LanguageId;
use crate::lsp::search::tests::TestPackageSearchApi;
use crate::lsp::urls::url_to_uri;
fn setup(
open_sources: &[(&str, &str, i32, LanguageId)],
fs_sources: &[(&str, &str)],
) -> Documents {
) -> DocumentModules {
let temp_dir = TempDir::new();
let cache = LspCache::new(Some(temp_dir.url().join(".deno_dir").unwrap()));
let mut documents = Documents::default();
documents.update_config(
let mut document_modules = DocumentModules::default();
document_modules.update_config(
&Default::default(),
&Default::default(),
&cache,
@ -851,7 +843,13 @@ mod tests {
for (specifier, source, version, language_id) in open_sources {
let specifier =
resolve_url(specifier).expect("failed to create specifier");
documents.open(specifier, *version, *language_id, (*source).into(), None);
let uri = url_to_uri(&specifier).unwrap();
document_modules.open_document(
uri,
*version,
*language_id,
(*source).into(),
);
}
for (specifier, source) in fs_sources {
let specifier =
@ -860,32 +858,10 @@ mod tests {
.global()
.set(&specifier, HashMap::default(), source.as_bytes())
.expect("could not cache file");
let document = documents
.get_or_load(&specifier, Some(&temp_dir.url().join("$").unwrap()));
assert!(document.is_some(), "source could not be setup");
let module = document_modules.module_for_specifier(&specifier, None);
assert!(module.is_some(), "source could not be setup");
}
documents
}
#[test]
fn test_get_relative_specifiers() {
let base = resolve_url("file:///a/b/c.ts").unwrap();
let specifiers = vec![
resolve_url("file:///a/b/c.ts").unwrap(),
resolve_url("file:///a/b/d.ts").unwrap(),
resolve_url("file:///a/c/c.ts").unwrap(),
resolve_url("file:///a/b/d/d.ts").unwrap(),
resolve_url("https://deno.land/x/a/b/c.ts").unwrap(),
];
assert_eq!(
get_relative_specifiers(&base, specifiers),
vec![
"./d.ts".to_string(),
"../c/c.ts".to_string(),
"./d/d.ts".to_string(),
"https://deno.land/x/a/b/c.ts".to_string(),
]
);
document_modules
}
#[test]
@ -940,7 +916,7 @@ mod tests {
}
#[tokio::test]
async fn test_get_workspace_completions() {
async fn test_get_remote_completions() {
let specifier = resolve_url("file:///a/b/c.ts").unwrap();
let range = lsp::Range {
start: lsp::Position {
@ -952,7 +928,7 @@ mod tests {
character: 21,
},
};
let documents = setup(
let document_modules = setup(
&[
(
"file:///a/b/c.ts",
@ -964,7 +940,11 @@ mod tests {
],
&[("https://deno.land/x/a/b/c.ts", "console.log(1);\n")],
);
let actual = get_workspace_completions(&specifier, "h", &range, &documents);
let module = document_modules
.module_for_specifier(&specifier, None)
.unwrap();
let actual =
get_remote_completions(&module, "h", &range, &document_modules);
assert_eq!(
actual,
vec![lsp::CompletionItem {

View file

@ -54,16 +54,19 @@ use deno_resolver::workspace::WorkspaceResolver;
use deno_runtime::deno_node::PackageJson;
use indexmap::IndexSet;
use lsp_types::ClientCapabilities;
use lsp_types::Uri;
use tower_lsp::lsp_types as lsp;
use super::logging::lsp_log;
use super::lsp_custom;
use super::urls::uri_to_url;
use super::urls::url_to_uri;
use crate::args::CliLockfile;
use crate::args::CliLockfileReadFromPathOptions;
use crate::args::ConfigFile;
use crate::args::LintFlags;
use crate::args::LintOptions;
use crate::cache::DenoDir;
use crate::file_fetcher::CliFileFetcher;
use crate::lsp::logging::lsp_warn;
use crate::sys::CliSys;
@ -821,20 +824,13 @@ impl WorkspaceSettings {
#[derive(Debug, Default, Clone)]
pub struct Settings {
pub unscoped: Arc<WorkspaceSettings>,
pub by_workspace_folder:
BTreeMap<ModuleSpecifier, Option<Arc<WorkspaceSettings>>>,
pub first_folder: Option<ModuleSpecifier>,
pub by_workspace_folder: BTreeMap<Arc<Url>, Option<Arc<WorkspaceSettings>>>,
pub first_folder: Option<Arc<Url>>,
}
impl Settings {
/// Returns `None` if the value should be deferred to the presence of a
/// `deno.json` file.
pub fn specifier_enabled(&self, specifier: &ModuleSpecifier) -> Option<bool> {
let Ok(path) = url_to_file_path(specifier) else {
// Non-file URLs are not disabled by these settings.
return Some(true);
};
let (settings, mut folder_uri) = self.get_for_specifier(specifier);
pub fn path_enabled(&self, path: &Path) -> Option<bool> {
let (settings, mut folder_uri) = self.get_for_path(path);
folder_uri = folder_uri.or(self.first_folder.as_ref());
let mut disable_paths = vec![];
let mut enable_paths = None;
@ -859,7 +855,7 @@ impl Settings {
} else if let Some(enable_paths) = &enable_paths {
for enable_path in enable_paths {
// Also enable if the checked path is a dir containing an enabled path.
if path.starts_with(enable_path) || enable_path.starts_with(&path) {
if path.starts_with(enable_path) || enable_path.starts_with(path) {
return Some(true);
}
}
@ -869,17 +865,24 @@ impl Settings {
}
}
/// Returns `None` if the value should be deferred to the presence of a
/// `deno.json` file.
pub fn specifier_enabled(&self, specifier: &ModuleSpecifier) -> Option<bool> {
let Ok(path) = url_to_file_path(specifier) else {
// Non-file URLs are not disabled by these settings.
return Some(true);
};
self.path_enabled(&path)
}
pub fn get_unscoped(&self) -> &WorkspaceSettings {
&self.unscoped
}
pub fn get_for_specifier(
pub fn get_for_path(
&self,
specifier: &ModuleSpecifier,
) -> (&WorkspaceSettings, Option<&ModuleSpecifier>) {
let Ok(path) = url_to_file_path(specifier) else {
return (&self.unscoped, self.first_folder.as_ref());
};
path: &Path,
) -> (&WorkspaceSettings, Option<&Arc<Url>>) {
for (folder_uri, settings) in self.by_workspace_folder.iter().rev() {
if let Some(settings) = settings {
let Ok(folder_path) = url_to_file_path(folder_uri) else {
@ -893,6 +896,23 @@ impl Settings {
(&self.unscoped, self.first_folder.as_ref())
}
pub fn get_for_uri(
&self,
uri: &Uri,
) -> (&WorkspaceSettings, Option<&Arc<Url>>) {
self.get_for_specifier(&uri_to_url(uri))
}
pub fn get_for_specifier(
&self,
specifier: &ModuleSpecifier,
) -> (&WorkspaceSettings, Option<&Arc<Url>>) {
let Ok(path) = url_to_file_path(specifier) else {
return (&self.unscoped, self.first_folder.as_ref());
};
self.get_for_path(&path)
}
pub fn enable_settings_hash(&self) -> u64 {
let mut hasher = FastInsecureHasher::new_without_deno_version();
let unscoped = self.get_unscoped();
@ -917,7 +937,7 @@ impl Settings {
pub struct Config {
pub client_capabilities: Arc<ClientCapabilities>,
pub settings: Arc<Settings>,
pub workspace_folders: Arc<Vec<(ModuleSpecifier, lsp::WorkspaceFolder)>>,
pub workspace_folders: Arc<Vec<(Arc<Url>, lsp::WorkspaceFolder)>>,
pub tree: ConfigTree,
}
@ -933,7 +953,7 @@ impl Config {
let name = root_url.path_segments().and_then(|s| s.last());
let name = name.unwrap_or_default().to_string();
folders.push((
root_url,
Arc::new(root_url),
lsp::WorkspaceFolder {
uri: root_uri,
name,
@ -946,7 +966,7 @@ impl Config {
pub fn set_workspace_folders(
&mut self,
folders: Vec<(ModuleSpecifier, lsp::WorkspaceFolder)>,
folders: Vec<(Arc<Url>, lsp::WorkspaceFolder)>,
) {
self.settings = Arc::new(Settings {
unscoped: self.settings.unscoped.clone(),
@ -962,7 +982,7 @@ impl Config {
pub fn set_workspace_settings(
&mut self,
unscoped: WorkspaceSettings,
folder_settings: Vec<(ModuleSpecifier, WorkspaceSettings)>,
folder_settings: Vec<(Arc<Url>, WorkspaceSettings)>,
) {
let mut by_folder = folder_settings.into_iter().collect::<HashMap<_, _>>();
self.settings = Arc::new(Settings {
@ -981,6 +1001,10 @@ impl Config {
self.settings.get_unscoped()
}
pub fn workspace_settings_for_uri(&self, uri: &Uri) -> &WorkspaceSettings {
self.settings.get_for_uri(uri).0
}
pub fn workspace_settings_for_specifier(
&self,
specifier: &ModuleSpecifier,
@ -1034,15 +1058,32 @@ impl Config {
|| settings.inlay_hints.enum_member_values.enabled
}
pub fn root_uri(&self) -> Option<&Url> {
pub fn root_url(&self) -> Option<&Arc<Url>> {
self.workspace_folders.first().map(|p| &p.0)
}
pub fn uri_enabled(&self, uri: &Uri) -> bool {
if uri.scheme().is_some_and(|s| s.eq_lowercase("deno")) {
return true;
}
self.specifier_enabled(&uri_to_url(uri))
}
pub fn specifier_enabled(&self, specifier: &ModuleSpecifier) -> bool {
if self.tree.in_global_npm_cache(specifier) {
return true;
}
let data = self.tree.data_for_specifier(specifier);
if let Some(data) = &data {
if let Ok(path) = specifier.to_file_path() {
if data.exclude_files.matches_path(&path) {
// deno_config's exclusion checks exclude vendor dirs invariably. We
// don't want that behavior here.
if data.exclude_files.matches_path(&path)
&& !data
.vendor_dir
.as_ref()
.is_some_and(|p| path.starts_with(p))
{
return false;
}
}
@ -1221,7 +1262,7 @@ impl ConfigData {
#[allow(clippy::too_many_arguments)]
async fn load(
specified_config: Option<&Path>,
scope: &ModuleSpecifier,
scope: &Arc<Url>,
settings: &Settings,
file_fetcher: &Arc<CliFileFetcher>,
// sync requirement is because the lsp requires sync
@ -1229,7 +1270,7 @@ impl ConfigData {
pkg_json_cache: &(dyn PackageJsonCache + Sync),
workspace_cache: &(dyn WorkspaceCache + Sync),
) -> Self {
let scope = Arc::new(scope.clone());
let scope = scope.clone();
let discover_result = match scope.to_file_path() {
Ok(scope_dir_path) => {
let paths = [scope_dir_path];
@ -1723,14 +1764,12 @@ impl ConfigData {
#[derive(Clone, Debug, Default)]
pub struct ConfigTree {
scopes: Arc<BTreeMap<ModuleSpecifier, Arc<ConfigData>>>,
scopes: Arc<BTreeMap<Arc<Url>, Arc<ConfigData>>>,
global_npm_cache_url: Option<Arc<Url>>,
}
impl ConfigTree {
pub fn scope_for_specifier(
&self,
specifier: &ModuleSpecifier,
) -> Option<&ModuleSpecifier> {
pub fn scope_for_specifier(&self, specifier: &Url) -> Option<&Arc<Url>> {
self
.scopes
.iter()
@ -1747,15 +1786,13 @@ impl ConfigTree {
.and_then(|s| self.scopes.get(s))
}
pub fn data_by_scope(
&self,
) -> &Arc<BTreeMap<ModuleSpecifier, Arc<ConfigData>>> {
pub fn data_by_scope(&self) -> &Arc<BTreeMap<Arc<Url>, Arc<ConfigData>>> {
&self.scopes
}
pub fn workspace_dir_for_specifier(
&self,
specifier: &ModuleSpecifier,
specifier: &Url,
) -> Option<&WorkspaceDirectory> {
self
.data_for_specifier(specifier)
@ -1778,10 +1815,7 @@ impl ConfigTree {
.collect()
}
pub fn fmt_config_for_specifier(
&self,
specifier: &ModuleSpecifier,
) -> Arc<FmtConfig> {
pub fn fmt_config_for_specifier(&self, specifier: &Url) -> Arc<FmtConfig> {
self
.data_for_specifier(specifier)
.map(|d| d.fmt_config.clone())
@ -1791,8 +1825,8 @@ impl ConfigTree {
/// Returns (scope_url, type).
pub fn watched_file_type(
&self,
specifier: &ModuleSpecifier,
) -> Option<(&ModuleSpecifier, ConfigWatchedFileType)> {
specifier: &Url,
) -> Option<(&Arc<Url>, ConfigWatchedFileType)> {
for (scope_url, data) in self.scopes.iter() {
if let Some(typ) = data.watched_files.get(specifier) {
return Some((scope_url, *typ));
@ -1801,7 +1835,7 @@ impl ConfigTree {
None
}
pub fn is_watched_file(&self, specifier: &ModuleSpecifier) -> bool {
pub fn is_watched_file(&self, specifier: &Url) -> bool {
let path = specifier.path();
if path.ends_with("/deno.json")
|| path.ends_with("/deno.jsonc")
@ -1856,14 +1890,29 @@ impl ConfigTree {
})
})
.collect();
lsp_custom::DidRefreshDenoConfigurationTreeNotificationParams { data }
let deno_dir_npm_folder_uri = self
.global_npm_cache_url
.as_ref()
.and_then(|s| url_to_uri(s).ok());
lsp_custom::DidRefreshDenoConfigurationTreeNotificationParams {
data,
deno_dir_npm_folder_uri,
}
}
pub fn in_global_npm_cache(&self, url: &Url) -> bool {
self
.global_npm_cache_url
.as_ref()
.is_some_and(|s| url.as_str().starts_with(s.as_str()))
}
pub async fn refresh(
&mut self,
settings: &Settings,
workspace_files: &IndexSet<ModuleSpecifier>,
workspace_files: &IndexSet<PathBuf>,
file_fetcher: &Arc<CliFileFetcher>,
deno_dir: &DenoDir,
) {
lsp_log!("Refreshing configuration tree...");
// since we're resolving a workspace multiple times in different
@ -1873,24 +1922,24 @@ impl ConfigTree {
let pkg_json_cache = PackageJsonMemCache::default();
let workspace_cache = WorkspaceMemCache::default();
let mut scopes = BTreeMap::new();
for (folder_uri, ws_settings) in &settings.by_workspace_folder {
for (folder_url, ws_settings) in &settings.by_workspace_folder {
let mut ws_settings = ws_settings.as_ref();
if Some(folder_uri) == settings.first_folder.as_ref() {
if Some(folder_url) == settings.first_folder.as_ref() {
ws_settings = ws_settings.or(Some(&settings.unscoped));
}
if let Some(ws_settings) = ws_settings {
let config_file_path = (|| {
let config_setting = ws_settings.config.as_ref()?;
let config_uri = folder_uri.join(config_setting).ok()?;
let config_uri = folder_url.join(config_setting).ok()?;
url_to_file_path(&config_uri).ok()
})();
if config_file_path.is_some() || ws_settings.import_map.is_some() {
scopes.insert(
folder_uri.clone(),
folder_url.clone(),
Arc::new(
ConfigData::load(
config_file_path.as_deref(),
folder_uri,
folder_url,
settings,
file_fetcher,
&deno_json_cache,
@ -1904,14 +1953,17 @@ impl ConfigTree {
}
}
for specifier in workspace_files {
if !(specifier.path().ends_with("/deno.json")
|| specifier.path().ends_with("/deno.jsonc")
|| specifier.path().ends_with("/package.json"))
for path in workspace_files {
let Ok(file_url) = Url::from_file_path(path) else {
continue;
};
if !(file_url.path().ends_with("/deno.json")
|| file_url.path().ends_with("/deno.jsonc")
|| file_url.path().ends_with("/package.json"))
{
continue;
}
let Ok(scope) = specifier.join(".") else {
let Ok(scope) = file_url.join(".").map(Arc::new) else {
continue;
};
if scopes.contains_key(&scope) {
@ -1944,11 +1996,15 @@ impl ConfigTree {
&workspace_cache,
)
.await;
scopes.insert(member_scope.as_ref().clone(), Arc::new(member_data));
scopes.insert(member_scope.clone(), Arc::new(member_data));
}
}
self.scopes = Arc::new(scopes);
self.global_npm_cache_url =
Url::from_directory_path(deno_dir.npm_folder_path())
.ok()
.map(Arc::new);
}
#[cfg(test)]
@ -1956,7 +2012,7 @@ impl ConfigTree {
use sys_traits::FsCreateDirAll;
use sys_traits::FsWrite;
let scope = config_file.specifier.join(".").unwrap();
let scope = Arc::new(config_file.specifier.join(".").unwrap());
let json_text = serde_json::to_string(&config_file.json).unwrap();
let memory_sys = sys_traits::impls::InMemorySys::default();
let config_path = url_to_file_path(&config_file.specifier).unwrap();
@ -1979,7 +2035,7 @@ impl ConfigTree {
let data = Arc::new(
ConfigData::load_inner(
workspace_dir,
Arc::new(scope.clone()),
scope.clone(),
&Default::default(),
None,
)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -57,6 +57,7 @@ pub struct DenoConfigurationData {
#[serde(rename_all = "camelCase")]
pub struct DidRefreshDenoConfigurationTreeNotificationParams {
pub data: Vec<DenoConfigurationData>,
pub deno_dir_npm_folder_uri: Option<lsp::Uri>,
}
pub enum DidRefreshDenoConfigurationTreeNotification {}

View file

@ -5,7 +5,7 @@
use deno_core::serde::Deserialize;
use deno_core::serde::Serialize;
use deno_core::ModuleSpecifier;
use lsp_types::Uri;
use once_cell::sync::Lazy;
use tower_lsp::lsp_types as lsp;
@ -150,7 +150,7 @@ pub static ALL_KNOWN_REFACTOR_ACTION_KINDS: Lazy<
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RefactorCodeActionData {
pub specifier: ModuleSpecifier,
pub uri: Uri,
pub range: lsp::Range,
pub refactor_name: String,
pub action_name: String,

View file

@ -299,7 +299,7 @@ impl LspScopeResolver {
#[derive(Debug, Default, Clone)]
pub struct LspResolver {
unscoped: Arc<LspScopeResolver>,
by_scope: BTreeMap<ModuleSpecifier, Arc<LspScopeResolver>>,
by_scope: BTreeMap<Arc<Url>, Arc<LspScopeResolver>>,
}
impl LspResolver {
@ -357,9 +357,7 @@ impl LspResolver {
pub async fn set_dep_info_by_scope(
&self,
dep_info_by_scope: &Arc<
BTreeMap<Option<ModuleSpecifier>, Arc<ScopeDepInfo>>,
>,
dep_info_by_scope: &Arc<BTreeMap<Option<Arc<Url>>, Arc<ScopeDepInfo>>>,
) {
for (scope, resolver) in [(None, &self.unscoped)]
.into_iter()

View file

@ -9,6 +9,7 @@ use deno_core::error::AnyError;
use deno_core::parking_lot::Mutex;
use deno_core::serde_json::json;
use deno_core::serde_json::Value;
use deno_core::url::Url;
use deno_core::ModuleSpecifier;
use deno_runtime::tokio_util::create_basic_runtime;
use tokio::sync::mpsc;
@ -22,7 +23,6 @@ use super::lsp_custom;
use crate::lsp::client::Client;
use crate::lsp::client::TestingNotification;
use crate::lsp::config;
use crate::lsp::documents::DocumentsFilter;
use crate::lsp::language_server::StateSnapshot;
use crate::lsp::performance::Performance;
use crate::lsp::urls::url_to_uri;
@ -63,7 +63,7 @@ impl TestServer {
pub fn new(
client: Client,
performance: Arc<Performance>,
maybe_root_uri: Option<ModuleSpecifier>,
maybe_root_url: Option<Arc<Url>>,
) -> Self {
let tests = Default::default();
@ -83,7 +83,7 @@ impl TestServer {
let tests = server.tests.clone();
let client = server.client.clone();
let performance = server.performance.clone();
let mru = maybe_root_uri.clone();
let mru = maybe_root_url.clone();
let _update_join_handle = thread::spawn(move || {
let runtime = create_basic_runtime();
@ -99,47 +99,62 @@ impl TestServer {
let mut keys: HashSet<ModuleSpecifier> =
tests.keys().cloned().collect();
for document in snapshot
.document_modules
.documents
.documents(DocumentsFilter::AllDiagnosable)
.filtered_docs(|d| d.is_file_like() && d.is_diagnosable())
{
let specifier = document.specifier();
if specifier.scheme() != "file" {
let Some(module) =
snapshot.document_modules.primary_module(&document)
else {
continue;
};
if module.specifier.scheme() != "file" {
continue;
}
if !snapshot.config.specifier_enabled_for_test(specifier) {
if !snapshot
.config
.specifier_enabled_for_test(&module.specifier)
{
continue;
}
keys.remove(specifier);
keys.remove(&module.specifier);
let script_version = document.script_version();
let valid =
if let Some((_, old_script_version)) = tests.get(specifier) {
old_script_version == &script_version
} else {
false
};
let valid = if let Some((_, old_script_version)) =
tests.get(&module.specifier)
{
old_script_version == &script_version
} else {
false
};
if !valid {
let was_empty = tests
.remove(specifier)
.remove(&module.specifier)
.map(|(tm, _)| tm.is_empty())
.unwrap_or(true);
let test_module = document
.maybe_test_module()
let test_module = module
.test_module()
.await
.map(|tm| tm.as_ref().clone())
.unwrap_or_else(|| TestModule::new(specifier.clone()));
.unwrap_or_else(|| {
TestModule::new(module.specifier.as_ref().clone())
});
if !test_module.is_empty() {
if let Ok(params) =
test_module.as_replace_notification(mru.as_ref())
test_module.as_replace_notification(mru.as_deref())
{
client.send_test_notification(params);
}
} else if !was_empty {
if let Ok(params) = as_delete_notification(specifier) {
if let Ok(params) =
as_delete_notification(&module.specifier)
{
client.send_test_notification(params);
}
}
tests
.insert(specifier.clone(), (test_module, script_version));
tests.insert(
module.specifier.as_ref().clone(),
(test_module, script_version),
);
}
}
for key in &keys {
@ -169,7 +184,7 @@ impl TestServer {
runs.get(&id).cloned()
};
if let Some(run) = maybe_run {
match run.exec(&client, maybe_root_uri.as_ref()).await {
match run.exec(&client, maybe_root_url.as_deref()).await {
Ok(_) => (),
Err(err) => {
client.show_message(lsp::MessageType::ERROR, err);

File diff suppressed because it is too large Load diff

View file

@ -1,36 +1,21 @@
// Copyright 2018-2025 the Deno authors. MIT license.
use std::collections::HashMap;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use deno_ast::MediaType;
use deno_config::UrlToFilePathError;
use deno_core::error::AnyError;
use deno_core::parking_lot::Mutex;
use deno_core::url::Position;
use deno_core::url::Url;
use deno_core::ModuleSpecifier;
use deno_path_util::url_to_file_path;
use lsp_types::Uri;
use once_cell::sync::Lazy;
use super::cache::LspCache;
use super::logging::lsp_warn;
/// Used in situations where a default URL needs to be used where otherwise a
/// panic is undesired.
pub static INVALID_SPECIFIER: Lazy<ModuleSpecifier> =
Lazy::new(|| ModuleSpecifier::parse("deno://invalid").unwrap());
/// Used in situations where a default URL needs to be used where otherwise a
/// panic is undesired.
pub static INVALID_URI: Lazy<Uri> =
Lazy::new(|| Uri::from_str("deno://invalid").unwrap());
/// Matches the `encodeURIComponent()` encoding from JavaScript, which matches
/// the component percent encoding set.
///
/// See: <https://url.spec.whatwg.org/#component-percent-encode-set>
const COMPONENT: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS
pub const COMPONENT: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS
.add(b' ')
.add(b'"')
.add(b'#')
@ -56,10 +41,24 @@ const COMPONENT: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS
.add(b'+')
.add(b',');
/// Characters that may be left unencoded in a `Url` path but not valid in a
/// `Uri` path.
/// Characters that are left unencoded in a `Url` path but will be encoded in a
/// VSCode URI.
const URL_TO_URI_PATH: &percent_encoding::AsciiSet =
&percent_encoding::CONTROLS
.add(b' ')
.add(b'!')
.add(b'$')
.add(b'&')
.add(b'\'')
.add(b'(')
.add(b')')
.add(b'*')
.add(b'+')
.add(b',')
.add(b':')
.add(b';')
.add(b'=')
.add(b'@')
.add(b'[')
.add(b']')
.add(b'^')
@ -75,73 +74,6 @@ const URL_TO_URI_QUERY: &percent_encoding::AsciiSet =
const URL_TO_URI_FRAGMENT: &percent_encoding::AsciiSet =
&URL_TO_URI_PATH.add(b'#').add(b'\\').add(b'{').add(b'}');
fn hash_data_specifier(specifier: &ModuleSpecifier) -> String {
let mut file_name_str = specifier.path().to_string();
if let Some(query) = specifier.query() {
file_name_str.push('?');
file_name_str.push_str(query);
}
deno_lib::util::checksum::gen(&[file_name_str.as_bytes()])
}
fn to_deno_uri(specifier: &Url) -> String {
let mut string = String::with_capacity(specifier.as_str().len() + 6);
string.push_str("deno:/");
string.push_str(specifier.scheme());
for p in specifier[Position::BeforeHost..].split('/') {
string.push('/');
string.push_str(
&percent_encoding::utf8_percent_encode(p, COMPONENT).to_string(),
);
}
string
}
fn from_deno_url(url: &Url) -> Option<Url> {
if url.scheme() != "deno" {
return None;
}
let mut segments = url.path_segments()?;
let mut string = String::with_capacity(url.as_str().len());
string.push_str(segments.next()?);
string.push_str("://");
string.push_str(
&percent_encoding::percent_decode(segments.next()?.as_bytes())
.decode_utf8()
.ok()?,
);
for segment in segments {
string.push('/');
string.push_str(
&percent_encoding::percent_decode(segment.as_bytes())
.decode_utf8()
.ok()?,
);
}
Url::parse(&string).ok()
}
#[derive(Debug, Default)]
struct LspUrlMapInner {
specifier_to_uri: HashMap<ModuleSpecifier, Uri>,
uri_to_specifier: HashMap<Uri, ModuleSpecifier>,
}
impl LspUrlMapInner {
fn put(&mut self, specifier: ModuleSpecifier, uri: Uri) {
self.uri_to_specifier.insert(uri.clone(), specifier.clone());
self.specifier_to_uri.insert(specifier, uri);
}
fn get_uri(&self, specifier: &ModuleSpecifier) -> Option<&Uri> {
self.specifier_to_uri.get(specifier)
}
fn get_specifier(&self, uri: &Uri) -> Option<&ModuleSpecifier> {
self.uri_to_specifier.get(uri)
}
}
pub fn uri_parse_unencoded(s: &str) -> Result<Uri, AnyError> {
url_to_uri(&Url::parse(s)?)
}
@ -150,10 +82,31 @@ pub fn url_to_uri(url: &Url) -> Result<Uri, AnyError> {
let components = deno_core::url::quirks::internal_components(url);
let mut input = String::with_capacity(url.as_str().len());
input.push_str(&url.as_str()[..components.path_start as usize]);
input.push_str(
&percent_encoding::utf8_percent_encode(url.path(), URL_TO_URI_PATH)
.to_string(),
);
if cfg!(windows) && url.scheme() == "file" {
let path = url.path();
let mut chars = path.chars();
let has_drive_letter = chars.next().is_some_and(|c| c == '/')
&& chars.next().is_some_and(|c| c.is_ascii_alphabetic())
&& chars.next().is_some_and(|c| c == ':')
&& chars.next().is_none_or(|c| c == '/');
if has_drive_letter {
input.push_str(&path[..3]);
input.push_str(
&percent_encoding::utf8_percent_encode(&path[3..], URL_TO_URI_PATH)
.to_string(),
);
} else {
input.push_str(
&percent_encoding::utf8_percent_encode(path, URL_TO_URI_PATH)
.to_string(),
);
}
} else {
input.push_str(
&percent_encoding::utf8_percent_encode(url.path(), URL_TO_URI_PATH)
.to_string(),
);
}
if let Some(query) = url.query() {
input.push('?');
input.push_str(
@ -174,283 +127,34 @@ pub fn url_to_uri(url: &Url) -> Result<Uri, AnyError> {
}
pub fn uri_to_url(uri: &Uri) -> Url {
Url::parse(uri.as_str()).unwrap()
}
#[derive(Debug, Clone, Copy)]
pub enum LspUrlKind {
File,
Folder,
}
/// A bi-directional map of URLs sent to the LSP client and internal module
/// specifiers. We need to map internal specifiers into `deno:` schema URLs
/// to allow the Deno language server to manage these as virtual documents.
#[derive(Debug, Default, Clone)]
pub struct LspUrlMap {
cache: LspCache,
inner: Arc<Mutex<LspUrlMapInner>>,
}
impl LspUrlMap {
pub fn set_cache(&mut self, cache: &LspCache) {
self.cache = cache.clone();
}
/// Normalize a specifier that is used internally within Deno (or tsc) to a
/// URL that can be handled as a "virtual" document by an LSP client.
pub fn specifier_to_uri(
&self,
specifier: &ModuleSpecifier,
file_referrer: Option<&ModuleSpecifier>,
) -> Result<Uri, AnyError> {
if let Some(file_url) =
self.cache.vendored_specifier(specifier, file_referrer)
(|| {
let scheme = uri.scheme()?;
if !scheme.eq_lowercase("untitled")
&& !scheme.eq_lowercase("vscode-notebook-cell")
&& !scheme.eq_lowercase("deno-notebook-cell")
{
return url_to_uri(&file_url);
return None;
}
let mut inner = self.inner.lock();
if let Some(uri) = inner.get_uri(specifier).cloned() {
Ok(uri)
} else {
let uri = if specifier.scheme() == "file" {
url_to_uri(specifier)?
} else {
let uri_str = if specifier.scheme() == "asset" {
format!("deno:/asset{}", specifier.path())
} else if specifier.scheme() == "data" {
let data_url =
deno_media_type::data_url::RawDataUrl::parse(specifier)?;
let media_type = data_url.media_type();
let extension = if media_type == MediaType::Unknown {
""
} else {
media_type.as_ts_extension()
};
format!(
"deno:/{}/data_url{}",
hash_data_specifier(specifier),
extension
)
} else {
to_deno_uri(specifier)
};
let uri = uri_parse_unencoded(&uri_str)?;
inner.put(specifier.clone(), uri.clone());
uri
};
Ok(uri)
}
}
/// Normalize URLs from the client, where "virtual" `deno:///` URLs are
/// converted into proper module specifiers, as well as handle situations
/// where the client encodes a file URL differently than Rust does by default
/// causing issues with string matching of URLs.
///
/// Note: Sometimes the url provided by the client may not have a trailing slash,
/// so we need to force it to in the mapping and nee to explicitly state whether
/// this is a file or directory url.
pub fn uri_to_specifier(
&self,
uri: &Uri,
kind: LspUrlKind,
) -> ModuleSpecifier {
let url = uri_to_url(uri);
if let Some(remote_url) = self.cache.unvendored_specifier(&url) {
return remote_url;
}
let mut inner = self.inner.lock();
if let Some(specifier) = inner.get_specifier(uri).cloned() {
return specifier;
}
let mut specifier = None;
if url.scheme() == "file" {
if let Ok(path) = url.to_file_path() {
specifier = Some(match kind {
LspUrlKind::Folder => Url::from_directory_path(path).unwrap(),
LspUrlKind::File => Url::from_file_path(path).unwrap(),
});
}
} else if let Some(s) = file_like_to_file_specifier(&url) {
specifier = Some(s);
} else if let Some(s) = from_deno_url(&url) {
specifier = Some(s);
}
let specifier = specifier.unwrap_or_else(|| url.clone());
inner.put(specifier.clone(), uri.clone());
specifier
}
}
/// Convert a e.g. `vscode-notebook-cell:` specifier to a `file:` specifier.
/// ```rust
/// assert_eq!(
/// file_like_to_file_specifier(
/// &Url::parse("vscode-notebook-cell:/path/to/file.ipynb#abc").unwrap(),
/// ),
/// Some(Url::parse("file:///path/to/file.ipynb?scheme=untitled#abc").unwrap()),
/// );
fn file_like_to_file_specifier(specifier: &Url) -> Option<Url> {
if matches!(
specifier.scheme(),
"untitled" | "vscode-notebook-cell" | "deno-notebook-cell"
) {
if let Ok(mut s) = ModuleSpecifier::parse(&format!(
Url::parse(&format!(
"file:///{}",
&specifier.as_str()[deno_core::url::quirks::internal_components(specifier)
.host_end as usize..].trim_start_matches('/'),
)) {
s.query_pairs_mut()
.append_pair("scheme", specifier.scheme());
return Some(s);
}
}
None
&uri.as_str()[uri.path_bounds.0 as usize..uri.path_bounds.1 as usize]
.trim_start_matches('/'),
))
.ok()
})()
.unwrap_or_else(|| Url::parse(uri.as_str()).unwrap())
}
#[cfg(test)]
mod tests {
use deno_core::resolve_url;
use super::*;
#[test]
fn test_hash_data_specifier() {
let fixture = resolve_url("data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=").unwrap();
let actual = hash_data_specifier(&fixture);
assert_eq!(
actual,
"c21c7fc382b2b0553dc0864aa81a3acacfb7b3d1285ab5ae76da6abec213fb37"
);
}
#[test]
fn test_lsp_url_map() {
let map = LspUrlMap::default();
let fixture = resolve_url("https://deno.land/x/pkg@1.0.0/mod.ts").unwrap();
let actual_uri = map
.specifier_to_uri(&fixture, None)
.expect("could not handle specifier");
assert_eq!(
actual_uri.as_str(),
"deno:/https/deno.land/x/pkg%401.0.0/mod.ts"
);
let actual_specifier = map.uri_to_specifier(&actual_uri, LspUrlKind::File);
assert_eq!(actual_specifier, fixture);
}
#[test]
fn test_lsp_url_reverse() {
let map = LspUrlMap::default();
let fixture =
Uri::from_str("deno:/https/deno.land/x/pkg%401.0.0/mod.ts").unwrap();
let actual_specifier = map.uri_to_specifier(&fixture, LspUrlKind::File);
let expected_specifier =
Url::parse("https://deno.land/x/pkg@1.0.0/mod.ts").unwrap();
assert_eq!(&actual_specifier, &expected_specifier);
let actual_uri = map.specifier_to_uri(&actual_specifier, None).unwrap();
assert_eq!(actual_uri, fixture);
}
#[test]
fn test_lsp_url_map_complex_encoding() {
// Test fix for #9741 - not properly encoding certain URLs
let map = LspUrlMap::default();
let fixture = resolve_url("https://cdn.skypack.dev/-/postcss@v8.2.9-E4SktPp9c0AtxrJHp8iV/dist=es2020,mode=types/lib/postcss.d.ts").unwrap();
let actual_uri = map
.specifier_to_uri(&fixture, None)
.expect("could not handle specifier");
assert_eq!(actual_uri.as_str(), "deno:/https/cdn.skypack.dev/-/postcss%40v8.2.9-E4SktPp9c0AtxrJHp8iV/dist%3Des2020%2Cmode%3Dtypes/lib/postcss.d.ts");
let actual_specifier = map.uri_to_specifier(&actual_uri, LspUrlKind::File);
assert_eq!(actual_specifier, fixture);
}
#[test]
fn test_lsp_url_map_data() {
let map = LspUrlMap::default();
let fixture = resolve_url("data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=").unwrap();
let actual_uri = map
.specifier_to_uri(&fixture, None)
.expect("could not handle specifier");
let expected_url = Url::parse("deno:/c21c7fc382b2b0553dc0864aa81a3acacfb7b3d1285ab5ae76da6abec213fb37/data_url.ts").unwrap();
assert_eq!(&uri_to_url(&actual_uri), &expected_url);
let actual_specifier = map.uri_to_specifier(&actual_uri, LspUrlKind::File);
assert_eq!(actual_specifier, fixture);
}
#[test]
fn test_lsp_url_map_host_with_port() {
let map = LspUrlMap::default();
let fixture = resolve_url("http://localhost:8000/mod.ts").unwrap();
let actual_uri = map
.specifier_to_uri(&fixture, None)
.expect("could not handle specifier");
assert_eq!(actual_uri.as_str(), "deno:/http/localhost%3A8000/mod.ts");
let actual_specifier = map.uri_to_specifier(&actual_uri, LspUrlKind::File);
assert_eq!(actual_specifier, fixture);
}
#[cfg(windows)]
#[test]
fn test_normalize_windows_path() {
let map = LspUrlMap::default();
let fixture = Uri::from_str(
"file:///c%3A/Users/deno/Desktop/file%20with%20spaces%20in%20name.txt",
)
.unwrap();
let actual = map.uri_to_specifier(&fixture, LspUrlKind::File);
let expected =
Url::parse("file:///C:/Users/deno/Desktop/file with spaces in name.txt")
.unwrap();
assert_eq!(actual, expected);
}
#[cfg(not(windows))]
#[test]
fn test_normalize_percent_encoded_path() {
let map = LspUrlMap::default();
let fixture = Uri::from_str(
"file:///Users/deno/Desktop/file%20with%20spaces%20in%20name.txt",
)
.unwrap();
let actual = map.uri_to_specifier(&fixture, LspUrlKind::File);
let expected =
Url::parse("file:///Users/deno/Desktop/file with spaces in name.txt")
.unwrap();
assert_eq!(actual, expected);
}
#[test]
fn test_normalize_deno_status() {
let map = LspUrlMap::default();
let fixture = Uri::from_str("deno:/status.md").unwrap();
let actual = map.uri_to_specifier(&fixture, LspUrlKind::File);
assert_eq!(actual.as_str(), fixture.as_str());
}
#[test]
fn test_file_like_to_file_specifier() {
assert_eq!(
file_like_to_file_specifier(
&Url::parse("vscode-notebook-cell:/path/to/file.ipynb#abc").unwrap(),
),
Some(
Url::parse(
"file:///path/to/file.ipynb?scheme=vscode-notebook-cell#abc"
)
.unwrap()
),
);
assert_eq!(
file_like_to_file_specifier(
&Url::parse("untitled:/path/to/file.ipynb#123").unwrap(),
),
Some(
Url::parse("file:///path/to/file.ipynb?scheme=untitled#123").unwrap()
),
);
}
pub fn uri_to_file_path(uri: &Uri) -> Result<PathBuf, UrlToFilePathError> {
url_to_file_path(&uri_to_url(uri))
}
pub fn uri_is_file_like(uri: &Uri) -> bool {
let Some(scheme) = uri.scheme() else {
return false;
};
scheme.eq_lowercase("file")
|| scheme.eq_lowercase("untitled")
|| scheme.eq_lowercase("vscode-notebook-cell")
|| scheme.eq_lowercase("deno-notebook-cell")
}

View file

@ -478,17 +478,17 @@ function serverRequestInner(id, method, args, scope, maybeChange) {
// (it's about to be invalidated anyway).
const cachedProjectVersion = PROJECT_VERSION_CACHE.get();
if (cachedProjectVersion && projectVersion !== cachedProjectVersion) {
return respond(id, [{}, null]);
return respond(id, [[], null]);
}
try {
/** @type {Record<string, any[]>} */
const diagnosticMap = {};
/** @type {any[][]} */
const diagnosticsList = [];
for (const specifier of args[0]) {
diagnosticMap[specifier] = fromTypeScriptDiagnostics([
diagnosticsList.push(fromTypeScriptDiagnostics([
...ls.getSemanticDiagnostics(specifier),
...ls.getSuggestionDiagnostics(specifier),
...ls.getSyntacticDiagnostics(specifier),
].filter(filterMapDiagnostic));
].filter(filterMapDiagnostic)));
}
let ambient =
ls.getProgram()?.getTypeChecker().getAmbientModules().map((symbol) =>
@ -502,18 +502,18 @@ function serverRequestInner(id, method, args, scope, maybeChange) {
} else {
ambientModulesCacheByScope.set(scope, ambient);
}
return respond(id, [diagnosticMap, ambient]);
return respond(id, [diagnosticsList, ambient]);
} catch (e) {
if (
!isCancellationError(e)
) {
return respond(
id,
[{}, null],
[[], null],
formatErrorWithArgs(e, [id, method, args, scope, maybeChange]),
);
}
return respond(id, [{}, null]);
return respond(id, [[], null]);
}
}
default:

View file

@ -895,96 +895,6 @@ fn lsp_import_map_node_specifiers() {
client.shutdown();
}
#[test]
#[timeout(300_000)]
fn lsp_format_vendor_path() {
let context = TestContextBuilder::new()
.use_http_server()
.use_temp_cwd()
.build();
// put this dependency in the global cache
context
.new_command()
.args("cache --allow-import http://localhost:4545/run/002_hello.ts")
.run()
.skip_output_check();
let temp_dir = context.temp_dir();
temp_dir.write("deno.json", json!({ "vendor": true }).to_string());
let mut client = context.new_lsp_command().build();
client.initialize_default();
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": r#"import "http://localhost:4545/run/002_hello.ts";"#,
},
}));
// copying from the global cache to the local cache requires explicitly
// running the cache command so that the checksums can be verified
assert_eq!(
diagnostics
.all()
.iter()
.map(|d| d.message.as_str())
.collect::<Vec<_>>(),
vec![
"Uncached or missing remote URL: http://localhost:4545/run/002_hello.ts"
]
);
client.write_request(
"workspace/executeCommand",
json!({
"command": "deno.cache",
"arguments": [[], temp_dir.url().join("file.ts").unwrap()],
}),
);
client.read_diagnostics();
assert!(temp_dir
.path()
.join("vendor/http_localhost_4545/run/002_hello.ts")
.exists());
client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("vendor/http_localhost_4545/run/002_hello.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": r#"console.log("Hello World");"#,
},
}));
let res = client.write_request(
"textDocument/formatting",
json!({
"textDocument": {
"uri": temp_dir.url().join("vendor/http_localhost_4545/run/002_hello.ts").unwrap(),
},
"options": {
"tabSize": 2,
"insertSpaces": true,
}
}),
);
assert_eq!(
res,
json!([{
"range": {
"start": {
"line": 0,
"character": 27,
},
"end": {
"line": 0,
"character": 27,
},
},
"newText": "\n",
}]),
);
client.shutdown();
}
// Regression test for https://github.com/denoland/deno/issues/19802.
// Disable the `workspace/configuration` capability. Ensure the LSP falls back
// to using `enablePaths` from the `InitializationOptions`.
@ -1088,11 +998,12 @@ fn lsp_did_refresh_deno_configuration_tree_notification() {
temp_dir.write("non_workspace1/deno.json", json!({}).to_string());
let mut client = context.new_lsp_command().build();
client.initialize_default();
let res = client
let mut res = client
.read_notification_with_method::<Value>(
"deno/didRefreshDenoConfigurationTree",
)
.unwrap();
res.as_object_mut().unwrap().remove("denoDirNpmFolderUri");
assert_eq!(
res,
json!({
@ -1142,11 +1053,12 @@ fn lsp_did_refresh_deno_configuration_tree_notification() {
}],
}));
client.read_diagnostics();
let res = client
let mut res = client
.read_notification_with_method::<Value>(
"deno/didRefreshDenoConfigurationTree",
)
.unwrap();
res.as_object_mut().unwrap().remove("denoDirNpmFolderUri");
assert_eq!(
res,
json!({
@ -1201,11 +1113,12 @@ fn lsp_did_refresh_deno_configuration_tree_notification() {
"disablePaths": ["non_workspace1"],
},
}));
let res = client
let mut res = client
.read_notification_with_method::<Value>(
"deno/didRefreshDenoConfigurationTree",
)
.unwrap();
res.as_object_mut().unwrap().remove("denoDirNpmFolderUri");
assert_eq!(
res,
json!({
@ -4138,7 +4051,7 @@ fn lsp_code_lens_references() {
"end": { "line": 0, "character": 7 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}, {
@ -4147,7 +4060,7 @@ fn lsp_code_lens_references() {
"end": { "line": 1, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}, {
@ -4156,7 +4069,7 @@ fn lsp_code_lens_references() {
"end": { "line": 3, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}, {
@ -4165,7 +4078,7 @@ fn lsp_code_lens_references() {
"end": { "line": 7, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}])
@ -4178,7 +4091,7 @@ fn lsp_code_lens_references() {
"end": { "line": 0, "character": 7 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}),
@ -4217,7 +4130,7 @@ fn lsp_code_lens_references() {
"end": { "line": 14, "character": 7 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}),
@ -4245,7 +4158,7 @@ fn lsp_code_lens_references() {
"end": { "line": 15, "character": 7 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}),
@ -4325,7 +4238,7 @@ fn lsp_code_lens_implementations() {
"end": { "line": 0, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "implementations"
}
}, {
@ -4334,7 +4247,7 @@ fn lsp_code_lens_implementations() {
"end": { "line": 0, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}, {
@ -4343,7 +4256,7 @@ fn lsp_code_lens_implementations() {
"end": { "line": 1, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}, {
@ -4352,7 +4265,7 @@ fn lsp_code_lens_implementations() {
"end": { "line": 4, "character": 7 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}, {
@ -4361,7 +4274,7 @@ fn lsp_code_lens_implementations() {
"end": { "line": 5, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}, {
@ -4370,7 +4283,7 @@ fn lsp_code_lens_implementations() {
"end": { "line": 10, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "implementations"
}
}, {
@ -4379,7 +4292,7 @@ fn lsp_code_lens_implementations() {
"end": { "line": 10, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}, {
@ -4388,7 +4301,7 @@ fn lsp_code_lens_implementations() {
"end": { "line": 11, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}])
@ -4401,7 +4314,7 @@ fn lsp_code_lens_implementations() {
"end": { "line": 0, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "implementations"
}
}),
@ -4438,7 +4351,7 @@ fn lsp_code_lens_implementations() {
"end": { "line": 10, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "implementations"
}
}),
@ -4816,7 +4729,7 @@ fn lsp_code_lens_non_doc_nav_tree() {
"end": { "line": 416, "character": 19 }
},
"data": {
"specifier": "asset:///lib.deno.shared_globals.d.ts",
"uri": "deno:/asset/lib.deno.shared_globals.d.ts",
"source": "references"
}
}),
@ -4865,7 +4778,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 0, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "implementations"
}
}, {
@ -4874,7 +4787,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 0, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}, {
@ -4883,7 +4796,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 1, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}, {
@ -4892,7 +4805,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 4, "character": 7 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}, {
@ -4901,7 +4814,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 5, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}, {
@ -4910,7 +4823,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 10, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "implementations"
}
}, {
@ -4919,7 +4832,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 10, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}, {
@ -4928,7 +4841,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 11, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}])
@ -4967,7 +4880,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 0, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "implementations"
}
}, {
@ -4976,7 +4889,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 0, "character": 11 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}, {
@ -4985,7 +4898,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 1, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}, {
@ -4994,7 +4907,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 4, "character": 7 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}, {
@ -5003,7 +4916,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 5, "character": 3 }
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"source": "references"
}
}])
@ -5338,7 +5251,7 @@ fn lsp_code_actions() {
"relatedInformation": []
}],
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"fixId": "fixAwaitInSyncFunction"
}
}])
@ -5361,7 +5274,7 @@ fn lsp_code_actions() {
"relatedInformation": []
}],
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"fixId": "fixAwaitInSyncFunction"
}
}),
@ -5424,7 +5337,7 @@ fn lsp_code_actions() {
}]
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"fixId": "fixAwaitInSyncFunction"
}
})
@ -6254,7 +6167,7 @@ fn lsp_jsr_code_action_move_to_new_file() {
},
"isPreferred": false,
"data": {
"specifier": file.url(),
"uri": file.url(),
"range": {
"start": { "line": 2, "character": 19 },
"end": { "line": 2, "character": 28 },
@ -6594,7 +6507,7 @@ fn lsp_asset_document_dom_code_action() {
let res = client.write_request(
"textDocument/codeAction",
json!({
"textDocument": { "uri": "asset:///lib.dom.d.ts" },
"textDocument": { "uri": "deno:/asset/lib.dom.d.ts" },
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 0, "character": 0 },
@ -6816,7 +6729,7 @@ export class DuckConfig {
"message": "Cannot find name 'DuckConfigOptions'."
}],
"data": {
"specifier": "file:///a/file00.ts",
"uri": "file:///a/file00.ts",
"fixId": "fixMissingImport"
}
}, {
@ -6874,7 +6787,7 @@ export class DuckConfig {
"message": "Cannot find name 'DuckConfig'."
}],
"data": {
"specifier": "file:///a/file00.ts",
"uri": "file:///a/file00.ts",
"fixId": "fixMissingImport"
}
}),
@ -6919,7 +6832,7 @@ export class DuckConfig {
}]
},
"data": {
"specifier": "file:///a/file00.ts",
"uri": "file:///a/file00.ts",
"fixId": "fixMissingImport"
}
})
@ -7150,7 +7063,7 @@ fn lsp_code_actions_refactor() {
"kind": "refactor.move.newFile",
"isPreferred": false,
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 1, "character": 0 }
@ -7163,7 +7076,7 @@ fn lsp_code_actions_refactor() {
"kind": "refactor.extract.function",
"isPreferred": false,
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 1, "character": 0 }
@ -7176,7 +7089,7 @@ fn lsp_code_actions_refactor() {
"kind": "refactor.extract.constant",
"isPreferred": false,
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 1, "character": 0 }
@ -7192,7 +7105,7 @@ fn lsp_code_actions_refactor() {
"reason": "This file already has a default export"
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 1, "character": 0 }
@ -7208,7 +7121,7 @@ fn lsp_code_actions_refactor() {
"reason": "This file already has a default export"
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 1, "character": 0 }
@ -7224,7 +7137,7 @@ fn lsp_code_actions_refactor() {
"reason": "Selection is not an import declaration."
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 1, "character": 0 }
@ -7240,7 +7153,7 @@ fn lsp_code_actions_refactor() {
"reason": "Selection is not an import declaration."
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 1, "character": 0 }
@ -7256,7 +7169,7 @@ fn lsp_code_actions_refactor() {
"reason": "Selection is not an import declaration."
},
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 1, "character": 0 }
@ -7273,7 +7186,7 @@ fn lsp_code_actions_refactor() {
"kind": "refactor.extract.interface",
"isPreferred": true,
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 0, "character": 7 },
"end": { "line": 0, "character": 33 }
@ -7311,7 +7224,7 @@ fn lsp_code_actions_refactor() {
},
"isPreferred": true,
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 0, "character": 7 },
"end": { "line": 0, "character": 33 }
@ -7448,7 +7361,7 @@ fn lsp_code_actions_imports_respects_fmt_config() {
"message": "Cannot find name 'DuckConfigOptions'."
}],
"data": {
"specifier": temp_dir.url().join("file00.ts").unwrap(),
"uri": temp_dir.url().join("file00.ts").unwrap(),
"fixId": "fixMissingImport"
}
}),
@ -7484,7 +7397,7 @@ fn lsp_code_actions_imports_respects_fmt_config() {
}]
},
"data": {
"specifier": temp_dir.url().join("file00.ts").unwrap(),
"uri": temp_dir.url().join("file00.ts").unwrap(),
"fixId": "fixMissingImport"
}
})
@ -7679,7 +7592,7 @@ fn lsp_code_actions_refactor_no_disabled_support() {
"kind": "refactor.move.newFile",
"isPreferred": false,
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 14, "character": 0 }
@ -7692,7 +7605,7 @@ fn lsp_code_actions_refactor_no_disabled_support() {
"kind": "refactor.extract.function",
"isPreferred": false,
"data": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 14, "character": 0 }
@ -7863,7 +7776,7 @@ fn lsp_completions() {
"insertTextFormat": 1,
"data": {
"tsc": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"position": 5,
"name": "build",
"useCodeSnippet": false
@ -7952,7 +7865,7 @@ fn lsp_completions_optional() {
"commitCharacters": [".", ",", ";", "("],
"data": {
"tsc": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"position": 79,
"name": "b",
"useCodeSnippet": false
@ -7972,7 +7885,7 @@ fn lsp_completions_optional() {
"insertText": "b",
"data": {
"tsc": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"position": 79,
"name": "b",
"useCodeSnippet": false
@ -8813,7 +8726,7 @@ fn lsp_infer_return_type() {
"kind": "refactor.rewrite.function.returnType",
"isPreferred": false,
"data": {
"specifier": file.url(),
"uri": file.url(),
"range": {
"start": { "line": 1, "character": 15 },
"end": { "line": 1, "character": 18 },
@ -8833,7 +8746,7 @@ fn lsp_infer_return_type() {
"kind": "refactor.rewrite.function.returnType",
"isPreferred": false,
"data": {
"specifier": file.url(),
"uri": file.url(),
"range": {
"start": { "line": 1, "character": 15 },
"end": { "line": 1, "character": 18 },
@ -9617,7 +9530,7 @@ fn lsp_completions_snippet() {
],
"data": {
"tsc": {
"specifier": "file:///a/a.tsx",
"uri": "file:///a/a.tsx",
"position": 87,
"name": "type",
"useCodeSnippet": false
@ -9645,7 +9558,7 @@ fn lsp_completions_snippet() {
],
"data": {
"tsc": {
"specifier": "file:///a/a.tsx",
"uri": "file:///a/a.tsx",
"position": 87,
"name": "type",
"useCodeSnippet": false
@ -9716,7 +9629,7 @@ fn lsp_completions_no_snippet() {
],
"data": {
"tsc": {
"specifier": "file:///a/a.tsx",
"uri": "file:///a/a.tsx",
"position": 87,
"name": "type",
"useCodeSnippet": false
@ -9819,7 +9732,7 @@ fn lsp_completions_npm() {
"insertTextFormat": 1,
"data": {
"tsc": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"position": 69,
"name": "MyClass",
"useCodeSnippet": false
@ -9836,7 +9749,7 @@ fn lsp_completions_npm() {
"insertTextFormat": 1,
"data": {
"tsc": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"position": 69,
"name": "MyClass",
"useCodeSnippet": false
@ -12731,7 +12644,7 @@ fn lsp_completions_complete_function_calls() {
"insertTextFormat": 1,
"data": {
"tsc": {
"specifier": "file:///a/file.ts",
"uri": "file:///a/file.ts",
"position": 3,
"name": "map",
"useCodeSnippet": true
@ -12757,6 +12670,107 @@ fn lsp_completions_complete_function_calls() {
client.shutdown();
}
// Regression test for https://github.com/denoland/vscode_deno/issues/1276.
#[test]
#[timeout(300_000)]
fn lsp_completions_private_class_fields() {
let context = TestContextBuilder::new()
.use_http_server()
.use_temp_cwd()
.build();
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": r#"
export class SomeClass {
#prop = 1;
foo() {
pro;
#pro;
}
}
"#,
},
}));
let list = client.get_completion_list(
"file:///a/file.ts",
(4, 15),
json!({
"triggerKind": 2,
"triggerCharacter": ".",
}),
);
assert!(!list.is_incomplete);
let item = list.items.iter().find(|i| i.label == "#prop").unwrap();
assert_eq!(
json!(item),
json!({
"label": "#prop",
"kind": 5,
"sortText": "14",
"filterText": "prop",
"insertText": "this.#prop",
"commitCharacters": [
".",
",",
";",
"("
],
"data": {
"tsc": {
"uri": "file:///a/file.ts",
"position": 88,
"name": "#prop",
"source": "ThisProperty/",
"useCodeSnippet": false,
},
},
}),
);
let list = client.get_completion_list(
"file:///a/file.ts",
(5, 16),
json!({
"triggerKind": 2,
"triggerCharacter": ".",
}),
);
assert!(!list.is_incomplete);
let item = list.items.iter().find(|i| i.label == "#prop").unwrap();
assert_eq!(
json!(item),
json!({
"label": "#prop",
"kind": 5,
"sortText": "14",
"filterText": "this.#prop",
"insertText": "this.#prop",
"commitCharacters": [
".",
",",
";",
"("
],
"data": {
"tsc": {
"uri": "file:///a/file.ts",
"position": 106,
"name": "#prop",
"source": "ThisProperty/",
"useCodeSnippet": false,
},
},
}),
);
client.shutdown();
}
#[test]
#[timeout(300_000)]
fn lsp_workspace_symbol() {
@ -14014,7 +14028,7 @@ fn lsp_closed_file_find_references_low_document_pre_load() {
);
// won't have results because the document won't be pre-loaded
assert_eq!(res, json!([]));
assert_eq!(res, json!(null));
client.shutdown();
}
@ -14069,7 +14083,7 @@ fn lsp_closed_file_find_references_excluded_path() {
);
// won't have results because the documents won't be pre-loaded
assert_eq!(res, json!([]));
assert_eq!(res, json!(null));
client.shutdown();
}
@ -14128,7 +14142,7 @@ fn lsp_data_urls_with_jsx_compiler_option() {
"end": { "line": 1, "character": 1 }
}
}, {
"uri": "deno:/ed0224c51f7e2a845dfc0941ed6959675e5e3e3d2a39b127f0ff569c1ffda8d8/data_url.ts",
"uri": "deno:/data_url/ed0224c51f7e2a845dfc0941ed6959675e5e3e3d2a39b127f0ff569c1ffda8d8.ts",
"range": {
"start": { "line": 0, "character": 7 },
"end": {"line": 0, "character": 14 },
@ -14881,7 +14895,7 @@ fn lsp_deno_json_scopes_node_modules_dir() {
assert_eq!(
res,
json!([{
"targetUri": canon_temp_dir.join("project1/node_modules/.deno/@denotest+add@1.0.0/node_modules/@denotest/add/index.d.ts").unwrap(),
"targetUri": canon_temp_dir.join("project1/node_modules/.deno/%40denotest%2Badd%401.0.0/node_modules/%40denotest/add/index.d.ts").unwrap(),
"targetRange": {
"start": {
"line": 0,
@ -14932,7 +14946,7 @@ fn lsp_deno_json_scopes_node_modules_dir() {
assert_eq!(
res,
json!([{
"targetUri": canon_temp_dir.join("project2/node_modules/.deno/@denotest+add@1.0.0/node_modules/@denotest/add/index.d.ts").unwrap(),
"targetUri": canon_temp_dir.join("project2/node_modules/.deno/%40denotest%2Badd%401.0.0/node_modules/%40denotest/add/index.d.ts").unwrap(),
"targetRange": {
"start": {
"line": 0,
@ -14983,7 +14997,7 @@ fn lsp_deno_json_scopes_node_modules_dir() {
assert_eq!(
res,
json!([{
"targetUri": canon_temp_dir.join("project2/project3/node_modules/.deno/@denotest+add@1.0.0/node_modules/@denotest/add/index.d.ts").unwrap(),
"targetUri": canon_temp_dir.join("project2/project3/node_modules/.deno/%40denotest%2Badd%401.0.0/node_modules/%40denotest/add/index.d.ts").unwrap(),
"targetRange": {
"start": {
"line": 0,
@ -16054,7 +16068,7 @@ fn lsp_deno_json_workspace_node_modules_dir() {
assert_eq!(
res,
json!([{
"targetUri": canon_temp_dir.join("project1/node_modules/.deno/@denotest+add@1.0.0/node_modules/@denotest/add/index.d.ts").unwrap(),
"targetUri": canon_temp_dir.join("project1/node_modules/.deno/%40denotest%2Badd%401.0.0/node_modules/%40denotest/add/index.d.ts").unwrap(),
"targetRange": {
"start": {
"line": 0,
@ -16832,7 +16846,7 @@ fn sloppy_imports_not_enabled() {
temp_dir.join("a").url_file(),
),
"data": {
"specifier": temp_dir.join("a").url_file(),
"uri": temp_dir.join("a").url_file(),
"to": temp_dir.join("a.ts").url_file(),
"message": "Add a '.ts' extension.",
},
@ -16859,7 +16873,7 @@ fn sloppy_imports_not_enabled() {
temp_dir.join("a").url_file(),
),
"data": {
"specifier": temp_dir.join("a").url_file(),
"uri": temp_dir.join("a").url_file(),
"to": temp_dir.join("a.ts").url_file(),
"message": "Add a '.ts' extension.",
},