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 GitHub
parent 76a18a2220
commit d91658b45e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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", "unicode-width 0.1.13",
"uuid", "uuid",
"walkdir", "walkdir",
"weak-table",
"winapi", "winapi",
"winres", "winres",
"zip", "zip",
@ -9402,6 +9403,12 @@ dependencies = [
"thiserror 2.0.12", "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]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.77" version = "0.3.77"

View file

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

View file

@ -177,6 +177,7 @@ typed-arena.workspace = true
unicode-width.workspace = true unicode-width.workspace = true
uuid = { workspace = true, features = ["serde"] } uuid = { workspace = true, features = ["serde"] }
walkdir.workspace = true walkdir.workspace = true
weak-table.workspace = true
zip = { workspace = true, features = ["deflate-flate2"] } zip = { workspace = true, features = ["deflate-flate2"] }
zstd.workspace = true 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(); let res = res.as_str().unwrap().to_string();
assert!(res.starts_with("# Deno Language Server Status")); assert!(res.starts_with("# Deno Language Server Status"));
let captures = re.captures(&res).unwrap(); let open_count = open_re
let count = captures.get(1).unwrap().as_str().parse::<usize>().unwrap(); .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); assert!(count > 1000, "count: {}", count);
client.shutdown(); client.shutdown();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,6 +9,7 @@ use deno_core::error::AnyError;
use deno_core::parking_lot::Mutex; use deno_core::parking_lot::Mutex;
use deno_core::serde_json::json; use deno_core::serde_json::json;
use deno_core::serde_json::Value; use deno_core::serde_json::Value;
use deno_core::url::Url;
use deno_core::ModuleSpecifier; use deno_core::ModuleSpecifier;
use deno_runtime::tokio_util::create_basic_runtime; use deno_runtime::tokio_util::create_basic_runtime;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@ -22,7 +23,6 @@ use super::lsp_custom;
use crate::lsp::client::Client; use crate::lsp::client::Client;
use crate::lsp::client::TestingNotification; use crate::lsp::client::TestingNotification;
use crate::lsp::config; use crate::lsp::config;
use crate::lsp::documents::DocumentsFilter;
use crate::lsp::language_server::StateSnapshot; use crate::lsp::language_server::StateSnapshot;
use crate::lsp::performance::Performance; use crate::lsp::performance::Performance;
use crate::lsp::urls::url_to_uri; use crate::lsp::urls::url_to_uri;
@ -63,7 +63,7 @@ impl TestServer {
pub fn new( pub fn new(
client: Client, client: Client,
performance: Arc<Performance>, performance: Arc<Performance>,
maybe_root_uri: Option<ModuleSpecifier>, maybe_root_url: Option<Arc<Url>>,
) -> Self { ) -> Self {
let tests = Default::default(); let tests = Default::default();
@ -83,7 +83,7 @@ impl TestServer {
let tests = server.tests.clone(); let tests = server.tests.clone();
let client = server.client.clone(); let client = server.client.clone();
let performance = server.performance.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 _update_join_handle = thread::spawn(move || {
let runtime = create_basic_runtime(); let runtime = create_basic_runtime();
@ -99,47 +99,62 @@ impl TestServer {
let mut keys: HashSet<ModuleSpecifier> = let mut keys: HashSet<ModuleSpecifier> =
tests.keys().cloned().collect(); tests.keys().cloned().collect();
for document in snapshot for document in snapshot
.document_modules
.documents .documents
.documents(DocumentsFilter::AllDiagnosable) .filtered_docs(|d| d.is_file_like() && d.is_diagnosable())
{ {
let specifier = document.specifier(); let Some(module) =
if specifier.scheme() != "file" { snapshot.document_modules.primary_module(&document)
else {
continue;
};
if module.specifier.scheme() != "file" {
continue; continue;
} }
if !snapshot.config.specifier_enabled_for_test(specifier) { if !snapshot
.config
.specifier_enabled_for_test(&module.specifier)
{
continue; continue;
} }
keys.remove(specifier); keys.remove(&module.specifier);
let script_version = document.script_version(); let script_version = document.script_version();
let valid = let valid = if let Some((_, old_script_version)) =
if let Some((_, old_script_version)) = tests.get(specifier) { tests.get(&module.specifier)
old_script_version == &script_version {
} else { old_script_version == &script_version
false } else {
}; false
};
if !valid { if !valid {
let was_empty = tests let was_empty = tests
.remove(specifier) .remove(&module.specifier)
.map(|(tm, _)| tm.is_empty()) .map(|(tm, _)| tm.is_empty())
.unwrap_or(true); .unwrap_or(true);
let test_module = document let test_module = module
.maybe_test_module() .test_module()
.await .await
.map(|tm| tm.as_ref().clone()) .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 !test_module.is_empty() {
if let Ok(params) = 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); client.send_test_notification(params);
} }
} else if !was_empty { } 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); client.send_test_notification(params);
} }
} }
tests tests.insert(
.insert(specifier.clone(), (test_module, script_version)); module.specifier.as_ref().clone(),
(test_module, script_version),
);
} }
} }
for key in &keys { for key in &keys {
@ -169,7 +184,7 @@ impl TestServer {
runs.get(&id).cloned() runs.get(&id).cloned()
}; };
if let Some(run) = maybe_run { 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(_) => (), Ok(_) => (),
Err(err) => { Err(err) => {
client.show_message(lsp::MessageType::ERROR, 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. // Copyright 2018-2025 the Deno authors. MIT license.
use std::collections::HashMap; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc;
use deno_ast::MediaType; use deno_config::UrlToFilePathError;
use deno_core::error::AnyError; use deno_core::error::AnyError;
use deno_core::parking_lot::Mutex;
use deno_core::url::Position;
use deno_core::url::Url; use deno_core::url::Url;
use deno_core::ModuleSpecifier; use deno_path_util::url_to_file_path;
use lsp_types::Uri; use lsp_types::Uri;
use once_cell::sync::Lazy;
use super::cache::LspCache;
use super::logging::lsp_warn; 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 /// Matches the `encodeURIComponent()` encoding from JavaScript, which matches
/// the component percent encoding set. /// the component percent encoding set.
/// ///
/// See: <https://url.spec.whatwg.org/#component-percent-encode-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'"') .add(b'"')
.add(b'#') .add(b'#')
@ -56,10 +41,24 @@ const COMPONENT: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS
.add(b'+') .add(b'+')
.add(b','); .add(b',');
/// Characters that may be left unencoded in a `Url` path but not valid in a /// Characters that are left unencoded in a `Url` path but will be encoded in a
/// `Uri` path. /// VSCode URI.
const URL_TO_URI_PATH: &percent_encoding::AsciiSet = const URL_TO_URI_PATH: &percent_encoding::AsciiSet =
&percent_encoding::CONTROLS &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']') .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 = const URL_TO_URI_FRAGMENT: &percent_encoding::AsciiSet =
&URL_TO_URI_PATH.add(b'#').add(b'\\').add(b'{').add(b'}'); &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> { pub fn uri_parse_unencoded(s: &str) -> Result<Uri, AnyError> {
url_to_uri(&Url::parse(s)?) 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 components = deno_core::url::quirks::internal_components(url);
let mut input = String::with_capacity(url.as_str().len()); let mut input = String::with_capacity(url.as_str().len());
input.push_str(&url.as_str()[..components.path_start as usize]); input.push_str(&url.as_str()[..components.path_start as usize]);
input.push_str( if cfg!(windows) && url.scheme() == "file" {
&percent_encoding::utf8_percent_encode(url.path(), URL_TO_URI_PATH) let path = url.path();
.to_string(), 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() { if let Some(query) = url.query() {
input.push('?'); input.push('?');
input.push_str( 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 { pub fn uri_to_url(uri: &Uri) -> Url {
Url::parse(uri.as_str()).unwrap() (|| {
} let scheme = uri.scheme()?;
if !scheme.eq_lowercase("untitled")
#[derive(Debug, Clone, Copy)] && !scheme.eq_lowercase("vscode-notebook-cell")
pub enum LspUrlKind { && !scheme.eq_lowercase("deno-notebook-cell")
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)
{ {
return url_to_uri(&file_url); return None;
} }
let mut inner = self.inner.lock(); Url::parse(&format!(
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!(
"file:///{}", "file:///{}",
&specifier.as_str()[deno_core::url::quirks::internal_components(specifier) &uri.as_str()[uri.path_bounds.0 as usize..uri.path_bounds.1 as usize]
.host_end as usize..].trim_start_matches('/'), .trim_start_matches('/'),
)) { ))
s.query_pairs_mut() .ok()
.append_pair("scheme", specifier.scheme()); })()
return Some(s); .unwrap_or_else(|| Url::parse(uri.as_str()).unwrap())
}
}
None
} }
#[cfg(test)] pub fn uri_to_file_path(uri: &Uri) -> Result<PathBuf, UrlToFilePathError> {
mod tests { url_to_file_path(&uri_to_url(uri))
use deno_core::resolve_url; }
use super::*; pub fn uri_is_file_like(uri: &Uri) -> bool {
let Some(scheme) = uri.scheme() else {
#[test] return false;
fn test_hash_data_specifier() { };
let fixture = resolve_url("data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=").unwrap(); scheme.eq_lowercase("file")
let actual = hash_data_specifier(&fixture); || scheme.eq_lowercase("untitled")
assert_eq!( || scheme.eq_lowercase("vscode-notebook-cell")
actual, || scheme.eq_lowercase("deno-notebook-cell")
"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()
),
);
}
} }

View file

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

View file

@ -895,96 +895,6 @@ fn lsp_import_map_node_specifiers() {
client.shutdown(); 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. // Regression test for https://github.com/denoland/deno/issues/19802.
// Disable the `workspace/configuration` capability. Ensure the LSP falls back // Disable the `workspace/configuration` capability. Ensure the LSP falls back
// to using `enablePaths` from the `InitializationOptions`. // 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()); temp_dir.write("non_workspace1/deno.json", json!({}).to_string());
let mut client = context.new_lsp_command().build(); let mut client = context.new_lsp_command().build();
client.initialize_default(); client.initialize_default();
let res = client let mut res = client
.read_notification_with_method::<Value>( .read_notification_with_method::<Value>(
"deno/didRefreshDenoConfigurationTree", "deno/didRefreshDenoConfigurationTree",
) )
.unwrap(); .unwrap();
res.as_object_mut().unwrap().remove("denoDirNpmFolderUri");
assert_eq!( assert_eq!(
res, res,
json!({ json!({
@ -1142,11 +1053,12 @@ fn lsp_did_refresh_deno_configuration_tree_notification() {
}], }],
})); }));
client.read_diagnostics(); client.read_diagnostics();
let res = client let mut res = client
.read_notification_with_method::<Value>( .read_notification_with_method::<Value>(
"deno/didRefreshDenoConfigurationTree", "deno/didRefreshDenoConfigurationTree",
) )
.unwrap(); .unwrap();
res.as_object_mut().unwrap().remove("denoDirNpmFolderUri");
assert_eq!( assert_eq!(
res, res,
json!({ json!({
@ -1201,11 +1113,12 @@ fn lsp_did_refresh_deno_configuration_tree_notification() {
"disablePaths": ["non_workspace1"], "disablePaths": ["non_workspace1"],
}, },
})); }));
let res = client let mut res = client
.read_notification_with_method::<Value>( .read_notification_with_method::<Value>(
"deno/didRefreshDenoConfigurationTree", "deno/didRefreshDenoConfigurationTree",
) )
.unwrap(); .unwrap();
res.as_object_mut().unwrap().remove("denoDirNpmFolderUri");
assert_eq!( assert_eq!(
res, res,
json!({ json!({
@ -4138,7 +4051,7 @@ fn lsp_code_lens_references() {
"end": { "line": 0, "character": 7 } "end": { "line": 0, "character": 7 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}, { }, {
@ -4147,7 +4060,7 @@ fn lsp_code_lens_references() {
"end": { "line": 1, "character": 3 } "end": { "line": 1, "character": 3 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}, { }, {
@ -4156,7 +4069,7 @@ fn lsp_code_lens_references() {
"end": { "line": 3, "character": 3 } "end": { "line": 3, "character": 3 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}, { }, {
@ -4165,7 +4078,7 @@ fn lsp_code_lens_references() {
"end": { "line": 7, "character": 3 } "end": { "line": 7, "character": 3 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}]) }])
@ -4178,7 +4091,7 @@ fn lsp_code_lens_references() {
"end": { "line": 0, "character": 7 } "end": { "line": 0, "character": 7 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}), }),
@ -4217,7 +4130,7 @@ fn lsp_code_lens_references() {
"end": { "line": 14, "character": 7 } "end": { "line": 14, "character": 7 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}), }),
@ -4245,7 +4158,7 @@ fn lsp_code_lens_references() {
"end": { "line": 15, "character": 7 } "end": { "line": 15, "character": 7 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}), }),
@ -4325,7 +4238,7 @@ fn lsp_code_lens_implementations() {
"end": { "line": 0, "character": 11 } "end": { "line": 0, "character": 11 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "implementations" "source": "implementations"
} }
}, { }, {
@ -4334,7 +4247,7 @@ fn lsp_code_lens_implementations() {
"end": { "line": 0, "character": 11 } "end": { "line": 0, "character": 11 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}, { }, {
@ -4343,7 +4256,7 @@ fn lsp_code_lens_implementations() {
"end": { "line": 1, "character": 3 } "end": { "line": 1, "character": 3 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}, { }, {
@ -4352,7 +4265,7 @@ fn lsp_code_lens_implementations() {
"end": { "line": 4, "character": 7 } "end": { "line": 4, "character": 7 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}, { }, {
@ -4361,7 +4274,7 @@ fn lsp_code_lens_implementations() {
"end": { "line": 5, "character": 3 } "end": { "line": 5, "character": 3 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}, { }, {
@ -4370,7 +4283,7 @@ fn lsp_code_lens_implementations() {
"end": { "line": 10, "character": 11 } "end": { "line": 10, "character": 11 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "implementations" "source": "implementations"
} }
}, { }, {
@ -4379,7 +4292,7 @@ fn lsp_code_lens_implementations() {
"end": { "line": 10, "character": 11 } "end": { "line": 10, "character": 11 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}, { }, {
@ -4388,7 +4301,7 @@ fn lsp_code_lens_implementations() {
"end": { "line": 11, "character": 3 } "end": { "line": 11, "character": 3 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}]) }])
@ -4401,7 +4314,7 @@ fn lsp_code_lens_implementations() {
"end": { "line": 0, "character": 11 } "end": { "line": 0, "character": 11 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "implementations" "source": "implementations"
} }
}), }),
@ -4438,7 +4351,7 @@ fn lsp_code_lens_implementations() {
"end": { "line": 10, "character": 11 } "end": { "line": 10, "character": 11 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "implementations" "source": "implementations"
} }
}), }),
@ -4816,7 +4729,7 @@ fn lsp_code_lens_non_doc_nav_tree() {
"end": { "line": 416, "character": 19 } "end": { "line": 416, "character": 19 }
}, },
"data": { "data": {
"specifier": "asset:///lib.deno.shared_globals.d.ts", "uri": "deno:/asset/lib.deno.shared_globals.d.ts",
"source": "references" "source": "references"
} }
}), }),
@ -4865,7 +4778,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 0, "character": 11 } "end": { "line": 0, "character": 11 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "implementations" "source": "implementations"
} }
}, { }, {
@ -4874,7 +4787,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 0, "character": 11 } "end": { "line": 0, "character": 11 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}, { }, {
@ -4883,7 +4796,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 1, "character": 3 } "end": { "line": 1, "character": 3 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}, { }, {
@ -4892,7 +4805,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 4, "character": 7 } "end": { "line": 4, "character": 7 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}, { }, {
@ -4901,7 +4814,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 5, "character": 3 } "end": { "line": 5, "character": 3 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}, { }, {
@ -4910,7 +4823,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 10, "character": 11 } "end": { "line": 10, "character": 11 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "implementations" "source": "implementations"
} }
}, { }, {
@ -4919,7 +4832,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 10, "character": 11 } "end": { "line": 10, "character": 11 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}, { }, {
@ -4928,7 +4841,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 11, "character": 3 } "end": { "line": 11, "character": 3 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}]) }])
@ -4967,7 +4880,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 0, "character": 11 } "end": { "line": 0, "character": 11 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "implementations" "source": "implementations"
} }
}, { }, {
@ -4976,7 +4889,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 0, "character": 11 } "end": { "line": 0, "character": 11 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}, { }, {
@ -4985,7 +4898,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 1, "character": 3 } "end": { "line": 1, "character": 3 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}, { }, {
@ -4994,7 +4907,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 4, "character": 7 } "end": { "line": 4, "character": 7 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}, { }, {
@ -5003,7 +4916,7 @@ fn lsp_nav_tree_updates() {
"end": { "line": 5, "character": 3 } "end": { "line": 5, "character": 3 }
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"source": "references" "source": "references"
} }
}]) }])
@ -5338,7 +5251,7 @@ fn lsp_code_actions() {
"relatedInformation": [] "relatedInformation": []
}], }],
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"fixId": "fixAwaitInSyncFunction" "fixId": "fixAwaitInSyncFunction"
} }
}]) }])
@ -5361,7 +5274,7 @@ fn lsp_code_actions() {
"relatedInformation": [] "relatedInformation": []
}], }],
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"fixId": "fixAwaitInSyncFunction" "fixId": "fixAwaitInSyncFunction"
} }
}), }),
@ -5424,7 +5337,7 @@ fn lsp_code_actions() {
}] }]
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"fixId": "fixAwaitInSyncFunction" "fixId": "fixAwaitInSyncFunction"
} }
}) })
@ -6254,7 +6167,7 @@ fn lsp_jsr_code_action_move_to_new_file() {
}, },
"isPreferred": false, "isPreferred": false,
"data": { "data": {
"specifier": file.url(), "uri": file.url(),
"range": { "range": {
"start": { "line": 2, "character": 19 }, "start": { "line": 2, "character": 19 },
"end": { "line": 2, "character": 28 }, "end": { "line": 2, "character": 28 },
@ -6594,7 +6507,7 @@ fn lsp_asset_document_dom_code_action() {
let res = client.write_request( let res = client.write_request(
"textDocument/codeAction", "textDocument/codeAction",
json!({ json!({
"textDocument": { "uri": "asset:///lib.dom.d.ts" }, "textDocument": { "uri": "deno:/asset/lib.dom.d.ts" },
"range": { "range": {
"start": { "line": 0, "character": 0 }, "start": { "line": 0, "character": 0 },
"end": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 },
@ -6816,7 +6729,7 @@ export class DuckConfig {
"message": "Cannot find name 'DuckConfigOptions'." "message": "Cannot find name 'DuckConfigOptions'."
}], }],
"data": { "data": {
"specifier": "file:///a/file00.ts", "uri": "file:///a/file00.ts",
"fixId": "fixMissingImport" "fixId": "fixMissingImport"
} }
}, { }, {
@ -6874,7 +6787,7 @@ export class DuckConfig {
"message": "Cannot find name 'DuckConfig'." "message": "Cannot find name 'DuckConfig'."
}], }],
"data": { "data": {
"specifier": "file:///a/file00.ts", "uri": "file:///a/file00.ts",
"fixId": "fixMissingImport" "fixId": "fixMissingImport"
} }
}), }),
@ -6919,7 +6832,7 @@ export class DuckConfig {
}] }]
}, },
"data": { "data": {
"specifier": "file:///a/file00.ts", "uri": "file:///a/file00.ts",
"fixId": "fixMissingImport" "fixId": "fixMissingImport"
} }
}) })
@ -7150,7 +7063,7 @@ fn lsp_code_actions_refactor() {
"kind": "refactor.move.newFile", "kind": "refactor.move.newFile",
"isPreferred": false, "isPreferred": false,
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"range": { "range": {
"start": { "line": 0, "character": 0 }, "start": { "line": 0, "character": 0 },
"end": { "line": 1, "character": 0 } "end": { "line": 1, "character": 0 }
@ -7163,7 +7076,7 @@ fn lsp_code_actions_refactor() {
"kind": "refactor.extract.function", "kind": "refactor.extract.function",
"isPreferred": false, "isPreferred": false,
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"range": { "range": {
"start": { "line": 0, "character": 0 }, "start": { "line": 0, "character": 0 },
"end": { "line": 1, "character": 0 } "end": { "line": 1, "character": 0 }
@ -7176,7 +7089,7 @@ fn lsp_code_actions_refactor() {
"kind": "refactor.extract.constant", "kind": "refactor.extract.constant",
"isPreferred": false, "isPreferred": false,
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"range": { "range": {
"start": { "line": 0, "character": 0 }, "start": { "line": 0, "character": 0 },
"end": { "line": 1, "character": 0 } "end": { "line": 1, "character": 0 }
@ -7192,7 +7105,7 @@ fn lsp_code_actions_refactor() {
"reason": "This file already has a default export" "reason": "This file already has a default export"
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"range": { "range": {
"start": { "line": 0, "character": 0 }, "start": { "line": 0, "character": 0 },
"end": { "line": 1, "character": 0 } "end": { "line": 1, "character": 0 }
@ -7208,7 +7121,7 @@ fn lsp_code_actions_refactor() {
"reason": "This file already has a default export" "reason": "This file already has a default export"
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"range": { "range": {
"start": { "line": 0, "character": 0 }, "start": { "line": 0, "character": 0 },
"end": { "line": 1, "character": 0 } "end": { "line": 1, "character": 0 }
@ -7224,7 +7137,7 @@ fn lsp_code_actions_refactor() {
"reason": "Selection is not an import declaration." "reason": "Selection is not an import declaration."
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"range": { "range": {
"start": { "line": 0, "character": 0 }, "start": { "line": 0, "character": 0 },
"end": { "line": 1, "character": 0 } "end": { "line": 1, "character": 0 }
@ -7240,7 +7153,7 @@ fn lsp_code_actions_refactor() {
"reason": "Selection is not an import declaration." "reason": "Selection is not an import declaration."
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"range": { "range": {
"start": { "line": 0, "character": 0 }, "start": { "line": 0, "character": 0 },
"end": { "line": 1, "character": 0 } "end": { "line": 1, "character": 0 }
@ -7256,7 +7169,7 @@ fn lsp_code_actions_refactor() {
"reason": "Selection is not an import declaration." "reason": "Selection is not an import declaration."
}, },
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"range": { "range": {
"start": { "line": 0, "character": 0 }, "start": { "line": 0, "character": 0 },
"end": { "line": 1, "character": 0 } "end": { "line": 1, "character": 0 }
@ -7273,7 +7186,7 @@ fn lsp_code_actions_refactor() {
"kind": "refactor.extract.interface", "kind": "refactor.extract.interface",
"isPreferred": true, "isPreferred": true,
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"range": { "range": {
"start": { "line": 0, "character": 7 }, "start": { "line": 0, "character": 7 },
"end": { "line": 0, "character": 33 } "end": { "line": 0, "character": 33 }
@ -7311,7 +7224,7 @@ fn lsp_code_actions_refactor() {
}, },
"isPreferred": true, "isPreferred": true,
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"range": { "range": {
"start": { "line": 0, "character": 7 }, "start": { "line": 0, "character": 7 },
"end": { "line": 0, "character": 33 } "end": { "line": 0, "character": 33 }
@ -7448,7 +7361,7 @@ fn lsp_code_actions_imports_respects_fmt_config() {
"message": "Cannot find name 'DuckConfigOptions'." "message": "Cannot find name 'DuckConfigOptions'."
}], }],
"data": { "data": {
"specifier": temp_dir.url().join("file00.ts").unwrap(), "uri": temp_dir.url().join("file00.ts").unwrap(),
"fixId": "fixMissingImport" "fixId": "fixMissingImport"
} }
}), }),
@ -7484,7 +7397,7 @@ fn lsp_code_actions_imports_respects_fmt_config() {
}] }]
}, },
"data": { "data": {
"specifier": temp_dir.url().join("file00.ts").unwrap(), "uri": temp_dir.url().join("file00.ts").unwrap(),
"fixId": "fixMissingImport" "fixId": "fixMissingImport"
} }
}) })
@ -7679,7 +7592,7 @@ fn lsp_code_actions_refactor_no_disabled_support() {
"kind": "refactor.move.newFile", "kind": "refactor.move.newFile",
"isPreferred": false, "isPreferred": false,
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"range": { "range": {
"start": { "line": 0, "character": 0 }, "start": { "line": 0, "character": 0 },
"end": { "line": 14, "character": 0 } "end": { "line": 14, "character": 0 }
@ -7692,7 +7605,7 @@ fn lsp_code_actions_refactor_no_disabled_support() {
"kind": "refactor.extract.function", "kind": "refactor.extract.function",
"isPreferred": false, "isPreferred": false,
"data": { "data": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"range": { "range": {
"start": { "line": 0, "character": 0 }, "start": { "line": 0, "character": 0 },
"end": { "line": 14, "character": 0 } "end": { "line": 14, "character": 0 }
@ -7863,7 +7776,7 @@ fn lsp_completions() {
"insertTextFormat": 1, "insertTextFormat": 1,
"data": { "data": {
"tsc": { "tsc": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"position": 5, "position": 5,
"name": "build", "name": "build",
"useCodeSnippet": false "useCodeSnippet": false
@ -7952,7 +7865,7 @@ fn lsp_completions_optional() {
"commitCharacters": [".", ",", ";", "("], "commitCharacters": [".", ",", ";", "("],
"data": { "data": {
"tsc": { "tsc": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"position": 79, "position": 79,
"name": "b", "name": "b",
"useCodeSnippet": false "useCodeSnippet": false
@ -7972,7 +7885,7 @@ fn lsp_completions_optional() {
"insertText": "b", "insertText": "b",
"data": { "data": {
"tsc": { "tsc": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"position": 79, "position": 79,
"name": "b", "name": "b",
"useCodeSnippet": false "useCodeSnippet": false
@ -8813,7 +8726,7 @@ fn lsp_infer_return_type() {
"kind": "refactor.rewrite.function.returnType", "kind": "refactor.rewrite.function.returnType",
"isPreferred": false, "isPreferred": false,
"data": { "data": {
"specifier": file.url(), "uri": file.url(),
"range": { "range": {
"start": { "line": 1, "character": 15 }, "start": { "line": 1, "character": 15 },
"end": { "line": 1, "character": 18 }, "end": { "line": 1, "character": 18 },
@ -8833,7 +8746,7 @@ fn lsp_infer_return_type() {
"kind": "refactor.rewrite.function.returnType", "kind": "refactor.rewrite.function.returnType",
"isPreferred": false, "isPreferred": false,
"data": { "data": {
"specifier": file.url(), "uri": file.url(),
"range": { "range": {
"start": { "line": 1, "character": 15 }, "start": { "line": 1, "character": 15 },
"end": { "line": 1, "character": 18 }, "end": { "line": 1, "character": 18 },
@ -9617,7 +9530,7 @@ fn lsp_completions_snippet() {
], ],
"data": { "data": {
"tsc": { "tsc": {
"specifier": "file:///a/a.tsx", "uri": "file:///a/a.tsx",
"position": 87, "position": 87,
"name": "type", "name": "type",
"useCodeSnippet": false "useCodeSnippet": false
@ -9645,7 +9558,7 @@ fn lsp_completions_snippet() {
], ],
"data": { "data": {
"tsc": { "tsc": {
"specifier": "file:///a/a.tsx", "uri": "file:///a/a.tsx",
"position": 87, "position": 87,
"name": "type", "name": "type",
"useCodeSnippet": false "useCodeSnippet": false
@ -9716,7 +9629,7 @@ fn lsp_completions_no_snippet() {
], ],
"data": { "data": {
"tsc": { "tsc": {
"specifier": "file:///a/a.tsx", "uri": "file:///a/a.tsx",
"position": 87, "position": 87,
"name": "type", "name": "type",
"useCodeSnippet": false "useCodeSnippet": false
@ -9819,7 +9732,7 @@ fn lsp_completions_npm() {
"insertTextFormat": 1, "insertTextFormat": 1,
"data": { "data": {
"tsc": { "tsc": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"position": 69, "position": 69,
"name": "MyClass", "name": "MyClass",
"useCodeSnippet": false "useCodeSnippet": false
@ -9836,7 +9749,7 @@ fn lsp_completions_npm() {
"insertTextFormat": 1, "insertTextFormat": 1,
"data": { "data": {
"tsc": { "tsc": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"position": 69, "position": 69,
"name": "MyClass", "name": "MyClass",
"useCodeSnippet": false "useCodeSnippet": false
@ -12731,7 +12644,7 @@ fn lsp_completions_complete_function_calls() {
"insertTextFormat": 1, "insertTextFormat": 1,
"data": { "data": {
"tsc": { "tsc": {
"specifier": "file:///a/file.ts", "uri": "file:///a/file.ts",
"position": 3, "position": 3,
"name": "map", "name": "map",
"useCodeSnippet": true "useCodeSnippet": true
@ -12757,6 +12670,107 @@ fn lsp_completions_complete_function_calls() {
client.shutdown(); 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] #[test]
#[timeout(300_000)] #[timeout(300_000)]
fn lsp_workspace_symbol() { 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 // won't have results because the document won't be pre-loaded
assert_eq!(res, json!([])); assert_eq!(res, json!(null));
client.shutdown(); 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 // won't have results because the documents won't be pre-loaded
assert_eq!(res, json!([])); assert_eq!(res, json!(null));
client.shutdown(); client.shutdown();
} }
@ -14128,7 +14142,7 @@ fn lsp_data_urls_with_jsx_compiler_option() {
"end": { "line": 1, "character": 1 } "end": { "line": 1, "character": 1 }
} }
}, { }, {
"uri": "deno:/ed0224c51f7e2a845dfc0941ed6959675e5e3e3d2a39b127f0ff569c1ffda8d8/data_url.ts", "uri": "deno:/data_url/ed0224c51f7e2a845dfc0941ed6959675e5e3e3d2a39b127f0ff569c1ffda8d8.ts",
"range": { "range": {
"start": { "line": 0, "character": 7 }, "start": { "line": 0, "character": 7 },
"end": {"line": 0, "character": 14 }, "end": {"line": 0, "character": 14 },
@ -14881,7 +14895,7 @@ fn lsp_deno_json_scopes_node_modules_dir() {
assert_eq!( assert_eq!(
res, res,
json!([{ 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": { "targetRange": {
"start": { "start": {
"line": 0, "line": 0,
@ -14932,7 +14946,7 @@ fn lsp_deno_json_scopes_node_modules_dir() {
assert_eq!( assert_eq!(
res, res,
json!([{ 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": { "targetRange": {
"start": { "start": {
"line": 0, "line": 0,
@ -14983,7 +14997,7 @@ fn lsp_deno_json_scopes_node_modules_dir() {
assert_eq!( assert_eq!(
res, res,
json!([{ 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": { "targetRange": {
"start": { "start": {
"line": 0, "line": 0,
@ -16054,7 +16068,7 @@ fn lsp_deno_json_workspace_node_modules_dir() {
assert_eq!( assert_eq!(
res, res,
json!([{ 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": { "targetRange": {
"start": { "start": {
"line": 0, "line": 0,
@ -16832,7 +16846,7 @@ fn sloppy_imports_not_enabled() {
temp_dir.join("a").url_file(), temp_dir.join("a").url_file(),
), ),
"data": { "data": {
"specifier": temp_dir.join("a").url_file(), "uri": temp_dir.join("a").url_file(),
"to": temp_dir.join("a.ts").url_file(), "to": temp_dir.join("a.ts").url_file(),
"message": "Add a '.ts' extension.", "message": "Add a '.ts' extension.",
}, },
@ -16859,7 +16873,7 @@ fn sloppy_imports_not_enabled() {
temp_dir.join("a").url_file(), temp_dir.join("a").url_file(),
), ),
"data": { "data": {
"specifier": temp_dir.join("a").url_file(), "uri": temp_dir.join("a").url_file(),
"to": temp_dir.join("a.ts").url_file(), "to": temp_dir.join("a.ts").url_file(),
"message": "Add a '.ts' extension.", "message": "Add a '.ts' extension.",
}, },