From b318d51822ddbecfd795e5e2717d15a456bafdf2 Mon Sep 17 00:00:00 2001 From: Nayeem Rahman Date: Mon, 17 Jun 2024 21:54:23 +0100 Subject: [PATCH] feat(lsp): multi deno.json resolver scopes (#24206) --- cli/lsp/cache.rs | 82 +++++-- cli/lsp/code_lens.rs | 2 +- cli/lsp/config.rs | 85 ++++---- cli/lsp/diagnostics.rs | 19 +- cli/lsp/documents.rs | 60 +++--- cli/lsp/language_server.rs | 57 ++--- cli/lsp/resolver.rs | 254 +++++++++++++++------- cli/lsp/tsc.rs | 34 ++- cli/lsp/urls.rs | 41 ++-- tests/integration/lsp_tests.rs | 383 ++++++++++++++++++++++++++++++--- 10 files changed, 762 insertions(+), 255 deletions(-) diff --git a/cli/lsp/cache.rs b/cli/lsp/cache.rs index d899cd7964..e6186030a1 100644 --- a/cli/lsp/cache.rs +++ b/cli/lsp/cache.rs @@ -11,6 +11,7 @@ use deno_runtime::fs_util::specifier_to_file_path; use deno_core::url::Url; use deno_core::ModuleSpecifier; +use std::collections::BTreeMap; use std::fs; use std::path::Path; use std::sync::Arc; @@ -29,13 +30,14 @@ pub const LSP_DISALLOW_GLOBAL_TO_LOCAL_COPY: deno_cache_dir::GlobalToLocalCopy = pub fn calculate_fs_version( cache: &LspCache, specifier: &ModuleSpecifier, + file_referrer: Option<&ModuleSpecifier>, ) -> Option { match specifier.scheme() { "npm" | "node" | "data" | "blob" => None, "file" => specifier_to_file_path(specifier) .ok() .and_then(|path| calculate_fs_version_at_path(&path)), - _ => calculate_fs_version_in_cache(cache, specifier), + _ => calculate_fs_version_in_cache(cache, specifier, file_referrer), } } @@ -56,8 +58,9 @@ pub fn calculate_fs_version_at_path(path: &Path) -> Option { fn calculate_fs_version_in_cache( cache: &LspCache, specifier: &ModuleSpecifier, + file_referrer: Option<&ModuleSpecifier>, ) -> Option { - let http_cache = cache.root_vendor_or_global(); + let http_cache = cache.for_specifier(file_referrer); let Ok(cache_key) = http_cache.cache_item_key(specifier) else { return Some("1".to_string()); }; @@ -77,7 +80,7 @@ fn calculate_fs_version_in_cache( pub struct LspCache { deno_dir: DenoDir, global: Arc, - root_vendor: Option>, + vendors_by_scope: BTreeMap>>, } impl Default for LspCache { @@ -107,18 +110,24 @@ impl LspCache { Self { deno_dir, global, - root_vendor: None, + vendors_by_scope: Default::default(), } } pub fn update_config(&mut self, config: &Config) { - self.root_vendor = config.tree.root_data().and_then(|data| { - let vendor_dir = data.vendor_dir.as_ref()?; - Some(Arc::new(LocalLspHttpCache::new( - vendor_dir.clone(), - self.global.clone(), - ))) - }); + self.vendors_by_scope = config + .tree + .data_by_scope() + .iter() + .map(|(scope, config_data)| { + ( + scope.clone(), + config_data.vendor_dir.as_ref().map(|v| { + Arc::new(LocalLspHttpCache::new(v.clone(), self.global.clone())) + }), + ) + }) + .collect(); } pub fn deno_dir(&self) -> &DenoDir { @@ -129,15 +138,50 @@ impl LspCache { &self.global } - pub fn root_vendor(&self) -> Option<&Arc> { - self.root_vendor.as_ref() - } - - pub fn root_vendor_or_global(&self) -> Arc { + pub fn for_specifier( + &self, + file_referrer: Option<&ModuleSpecifier>, + ) -> Arc { + let Some(file_referrer) = file_referrer else { + return self.global.clone(); + }; self - .root_vendor - .as_ref() - .map(|v| v.clone() as _) + .vendors_by_scope + .iter() + .rfind(|(s, _)| file_referrer.as_str().starts_with(s.as_str())) + .and_then(|(_, v)| v.clone().map(|v| v as _)) .unwrap_or(self.global.clone() as _) } + + pub fn vendored_specifier( + &self, + specifier: &ModuleSpecifier, + file_referrer: Option<&ModuleSpecifier>, + ) -> Option { + let file_referrer = file_referrer?; + if !matches!(specifier.scheme(), "http" | "https") { + return None; + } + let vendor = self + .vendors_by_scope + .iter() + .rfind(|(s, _)| file_referrer.as_str().starts_with(s.as_str()))? + .1 + .as_ref()?; + vendor.get_file_url(specifier) + } + + pub fn unvendored_specifier( + &self, + specifier: &ModuleSpecifier, + ) -> Option { + let path = specifier_to_file_path(specifier).ok()?; + let vendor = self + .vendors_by_scope + .iter() + .rfind(|(s, _)| specifier.as_str().starts_with(s.as_str()))? + .1 + .as_ref()?; + vendor.get_remote_url(&path) + } } diff --git a/cli/lsp/code_lens.rs b/cli/lsp/code_lens.rs index c7e0c59bbd..21daf0ac48 100644 --- a/cli/lsp/code_lens.rs +++ b/cli/lsp/code_lens.rs @@ -340,7 +340,7 @@ async fn resolve_references_code_lens( locations.push( reference .entry - .to_location(asset_or_doc.line_index(), &language_server.url_map), + .to_location(asset_or_doc.line_index(), language_server), ); } Ok(locations) diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index 327f725e4d..1544ebdf34 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -1568,27 +1568,13 @@ impl ConfigData { #[derive(Clone, Debug, Default)] pub struct ConfigTree { first_folder: Option, - scopes: Arc>, + scopes: Arc>>, } impl ConfigTree { - pub fn root_scope(&self) -> Option<&ModuleSpecifier> { - self.first_folder.as_ref() - } - - pub fn root_data(&self) -> Option<&ConfigData> { - self.first_folder.as_ref().and_then(|s| self.scopes.get(s)) - } - pub fn root_ts_config(&self) -> Arc { - self - .root_data() - .map(|d| d.ts_config.clone()) - .unwrap_or_default() - } - - pub fn root_import_map(&self) -> Option<&Arc> { - self.root_data().and_then(|d| d.import_map.as_ref()) + let root_data = self.first_folder.as_ref().and_then(|s| self.scopes.get(s)); + root_data.map(|d| d.ts_config.clone()).unwrap_or_default() } pub fn scope_for_specifier( @@ -1599,19 +1585,20 @@ impl ConfigTree { .scopes .keys() .rfind(|s| specifier.as_str().starts_with(s.as_str())) - .or(self.first_folder.as_ref()) } pub fn data_for_specifier( &self, specifier: &ModuleSpecifier, - ) -> Option<&ConfigData> { + ) -> Option<&Arc> { self .scope_for_specifier(specifier) .and_then(|s| self.scopes.get(s)) } - pub fn data_by_scope(&self) -> &Arc> { + pub fn data_by_scope( + &self, + ) -> &Arc>> { &self.scopes } @@ -1694,14 +1681,16 @@ impl ConfigTree { if let Ok(config_uri) = folder_uri.join(config_path) { scopes.insert( folder_uri.clone(), - ConfigData::load( - Some(&config_uri), - folder_uri, - None, - settings, - Some(file_fetcher), - ) - .await, + Arc::new( + ConfigData::load( + Some(&config_uri), + folder_uri, + None, + settings, + Some(file_fetcher), + ) + .await, + ), ); } } @@ -1756,10 +1745,10 @@ impl ConfigTree { Some(file_fetcher), ) .await; - scopes.insert(member_scope.clone(), member_data); + scopes.insert(member_scope.clone(), Arc::new(member_data)); } } - scopes.insert(scope, data); + scopes.insert(scope, Arc::new(data)); } for folder_uri in settings.by_workspace_folder.keys() { @@ -1769,14 +1758,16 @@ impl ConfigTree { { scopes.insert( folder_uri.clone(), - ConfigData::load( - None, - folder_uri, - None, - settings, - Some(file_fetcher), - ) - .await, + Arc::new( + ConfigData::load( + None, + folder_uri, + None, + settings, + Some(file_fetcher), + ) + .await, + ), ); } } @@ -1787,14 +1778,16 @@ impl ConfigTree { #[cfg(test)] pub async fn inject_config_file(&mut self, config_file: ConfigFile) { let scope = config_file.specifier.join(".").unwrap(); - let data = ConfigData::load_inner( - Some(config_file), - &scope, - None, - &Default::default(), - None, - ) - .await; + let data = Arc::new( + ConfigData::load_inner( + Some(config_file), + &scope, + None, + &Default::default(), + None, + ) + .await, + ); self.first_folder = Some(scope.clone()); self.scopes = Arc::new([(scope, data)].into_iter().collect()); } diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs index e76d2c5b0e..f2a22e570a 100644 --- a/cli/lsp/diagnostics.rs +++ b/cli/lsp/diagnostics.rs @@ -5,6 +5,7 @@ use super::client::Client; use super::config::Config; use super::documents; use super::documents::Document; +use super::documents::Documents; use super::documents::DocumentsFilter; use super::language_server; use super::language_server::StateSnapshot; @@ -120,6 +121,7 @@ impl DiagnosticsPublisher { source: DiagnosticSource, diagnostics: DiagnosticVec, url_map: &LspUrlMap, + documents: &Documents, token: &CancellationToken, ) -> usize { let mut diagnostics_by_specifier = @@ -153,11 +155,12 @@ impl DiagnosticsPublisher { self .state .update(&record.specifier, version, &all_specifier_diagnostics); + let file_referrer = documents.get_file_referrer(&record.specifier); self .client .publish_diagnostics( url_map - .normalize_specifier(&record.specifier) + .normalize_specifier(&record.specifier, file_referrer.as_deref()) .unwrap_or(LspClientUrl::new(record.specifier)), all_specifier_diagnostics, version, @@ -183,11 +186,12 @@ impl DiagnosticsPublisher { if let Some(removed_value) = maybe_removed_value { // clear out any diagnostics for this specifier self.state.update(specifier, removed_value.version, &[]); + let file_referrer = documents.get_file_referrer(specifier); self .client .publish_diagnostics( url_map - .normalize_specifier(specifier) + .normalize_specifier(specifier, file_referrer.as_deref()) .unwrap_or_else(|_| LspClientUrl::new(specifier.clone())), Vec::new(), removed_value.version, @@ -519,6 +523,7 @@ impl DiagnosticsServer { DiagnosticSource::Ts, diagnostics, &url_map, + snapshot.documents.as_ref(), &token, ) .await; @@ -556,6 +561,7 @@ impl DiagnosticsServer { let mark = performance.mark("lsp.update_diagnostics_deps"); let diagnostics = spawn_blocking({ let token = token.clone(); + let snapshot = snapshot.clone(); move || generate_deno_diagnostics(&snapshot, &config, token) }) .await @@ -568,6 +574,7 @@ impl DiagnosticsServer { DiagnosticSource::Deno, diagnostics, &url_map, + snapshot.documents.as_ref(), &token, ) .await; @@ -605,6 +612,7 @@ impl DiagnosticsServer { let mark = performance.mark("lsp.update_diagnostics_lint"); let diagnostics = spawn_blocking({ let token = token.clone(); + let snapshot = snapshot.clone(); move || generate_lint_diagnostics(&snapshot, &config, token) }) .await @@ -617,6 +625,7 @@ impl DiagnosticsServer { DiagnosticSource::Lint, diagnostics, &url_map, + snapshot.documents.as_ref(), &token, ) .await; @@ -1466,7 +1475,11 @@ fn diagnose_dependency( return; // ignore, surface typescript errors instead } - let import_map = snapshot.config.tree.root_import_map(); + let import_map = snapshot + .config + .tree + .data_for_specifier(referrer_doc.file_referrer().unwrap_or(referrer)) + .and_then(|d| d.import_map.as_ref()); if let Some(import_map) = import_map { if let Resolution::Ok(resolved) = &dependency.maybe_code { if let Some(to) = import_map.lookup(&resolved.specifier, referrer) { diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index 6d7c2ca7ef..93fcfe8084 100644 --- a/cli/lsp/documents.rs +++ b/cli/lsp/documents.rs @@ -303,6 +303,10 @@ impl Document { cache: &Arc, file_referrer: Option, ) -> Arc { + let file_referrer = Some(&specifier) + .filter(|s| s.scheme() == "file") + .cloned() + .or(file_referrer); let media_type = resolve_media_type( &specifier, maybe_headers.as_ref(), @@ -336,9 +340,13 @@ impl Document { Arc::new(Self { config, dependencies, - file_referrer: file_referrer.filter(|_| specifier.scheme() != "file"), + maybe_fs_version: calculate_fs_version( + cache, + &specifier, + file_referrer.as_ref(), + ), + file_referrer, maybe_types_dependency, - maybe_fs_version: calculate_fs_version(cache, &specifier), line_index, maybe_language_id, maybe_headers, @@ -540,7 +548,11 @@ impl Document { config: self.config.clone(), specifier: self.specifier.clone(), file_referrer: self.file_referrer.clone(), - maybe_fs_version: calculate_fs_version(cache, &self.specifier), + maybe_fs_version: calculate_fs_version( + cache, + &self.specifier, + self.file_referrer.as_ref(), + ), maybe_language_id: self.maybe_language_id, dependencies: self.dependencies.clone(), maybe_types_dependency: self.maybe_types_dependency.clone(), @@ -563,7 +575,11 @@ impl Document { config: self.config.clone(), specifier: self.specifier.clone(), file_referrer: self.file_referrer.clone(), - maybe_fs_version: calculate_fs_version(cache, &self.specifier), + maybe_fs_version: calculate_fs_version( + cache, + &self.specifier, + self.file_referrer.as_ref(), + ), maybe_language_id: self.maybe_language_id, dependencies: self.dependencies.clone(), maybe_types_dependency: self.maybe_types_dependency.clone(), @@ -766,7 +782,10 @@ impl FileSystemDocuments { cache: &Arc, file_referrer: Option<&ModuleSpecifier>, ) -> Option> { - let new_fs_version = calculate_fs_version(cache, specifier); + let file_referrer = Some(specifier) + .filter(|s| s.scheme() == "file") + .or(file_referrer); + let new_fs_version = calculate_fs_version(cache, specifier, file_referrer); let old_doc = self.docs.get(specifier).map(|v| v.value().clone()); let dirty = match &old_doc { None => true, @@ -830,7 +849,7 @@ impl FileSystemDocuments { file_referrer.cloned(), ) } else { - let http_cache = cache.root_vendor_or_global(); + let http_cache = cache.for_specifier(file_referrer); let cache_key = http_cache.cache_item_key(specifier).ok()?; let bytes = http_cache .read_file_bytes(&cache_key, None, LSP_DISALLOW_GLOBAL_TO_LOCAL_COPY) @@ -1089,7 +1108,7 @@ impl Documents { .map(|p| p.is_file()) .unwrap_or(false); } - if self.cache.root_vendor_or_global().contains(&specifier) { + if self.cache.for_specifier(file_referrer).contains(&specifier) { return true; } } @@ -1335,8 +1354,7 @@ impl Documents { let mut visit_doc = |doc: &Arc| { let scope = doc .file_referrer() - .and_then(|r| self.config.tree.scope_for_specifier(r)) - .or(self.config.tree.root_scope()); + .and_then(|r| self.config.tree.scope_for_specifier(r)); let reqs = npm_reqs_by_scope.entry(scope.cloned()).or_default(); for dependency in doc.dependencies().values() { if let Some(dep) = dependency.get_code() { @@ -1367,21 +1385,15 @@ impl Documents { } // fill the reqs from the lockfile - // TODO(nayeemrmn): Iterate every lockfile here for multi-deno.json. - if let Some(lockfile) = self - .config - .tree - .root_data() - .and_then(|d| d.lockfile.as_ref()) - { - let reqs = npm_reqs_by_scope - .entry(self.config.tree.root_scope().cloned()) - .or_default(); - let lockfile = lockfile.lock(); - for key in lockfile.content.packages.specifiers.keys() { - if let Some(key) = key.strip_prefix("npm:") { - if let Ok(req) = PackageReq::from_str(key) { - reqs.insert(req); + for (scope, config_data) in self.config.tree.data_by_scope().as_ref() { + if let Some(lockfile) = config_data.lockfile.as_ref() { + let reqs = npm_reqs_by_scope.entry(Some(scope.clone())).or_default(); + let lockfile = lockfile.lock(); + for key in lockfile.content.packages.specifiers.keys() { + if let Some(key) = key.strip_prefix("npm:") { + if let Ok(req) = PackageReq::from_str(key) { + reqs.insert(req); + } } } } diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index b8194109de..be1b27cdaa 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -682,7 +682,7 @@ impl Inner { pub fn update_cache(&mut self) { let mark = self.performance.mark("lsp.update_cache"); self.cache.update_config(&self.config); - self.url_map.set_cache(self.cache.root_vendor().cloned()); + self.url_map.set_cache(&self.cache); self.performance.measure(mark); } @@ -1134,11 +1134,9 @@ impl Inner { let package_reqs = self.documents.npm_reqs_by_scope(); let resolver = self.resolver.clone(); // spawn due to the lsp's `Send` requirement - let handle = - spawn(async move { resolver.set_npm_reqs(&package_reqs).await }); - if let Err(err) = handle.await.unwrap() { - lsp_warn!("Could not set npm package requirements. {:#}", err); - } + spawn(async move { resolver.set_npm_reqs(&package_reqs).await }) + .await + .ok(); } async fn did_close(&mut self, params: DidCloseTextDocumentParams) { @@ -1818,11 +1816,15 @@ impl Inner { pub fn get_ts_response_import_mapper( &self, - _referrer: &ModuleSpecifier, + file_referrer: &ModuleSpecifier, ) -> TsResponseImportMapper { TsResponseImportMapper::new( &self.documents, - self.config.tree.root_import_map().map(|i| i.as_ref()), + self + .config + .tree + .data_for_specifier(file_referrer) + .and_then(|d| d.import_map.as_ref().map(|i| i.as_ref())), self.resolver.as_ref(), ) } @@ -1999,11 +2001,7 @@ impl Inner { self.get_asset_or_document(&reference_specifier)?; asset_or_doc.line_index() }; - results.push( - reference - .entry - .to_location(reference_line_index, &self.url_map), - ); + results.push(reference.entry.to_location(reference_line_index, self)); } self.performance.measure(mark); @@ -2125,6 +2123,10 @@ impl Inner { .map(|s| s.suggest.include_completions_for_import_statements) .unwrap_or(true) { + let file_referrer = asset_or_doc + .document() + .and_then(|d| d.file_referrer()) + .unwrap_or(&specifier); response = completions::get_import_completions( &specifier, ¶ms.text_document_position.position, @@ -2135,7 +2137,11 @@ impl Inner { &self.npm_search_api, &self.documents, self.resolver.as_ref(), - self.config.tree.root_import_map().map(|i| i.as_ref()), + self + .config + .tree + .data_for_specifier(file_referrer) + .and_then(|d| d.import_map.as_ref().map(|i| i.as_ref())), ) .await; } @@ -3442,7 +3448,7 @@ impl Inner { let mark = self .performance .mark_with_args("lsp.cache", (&specifiers, &referrer)); - let config_data = self.config.tree.root_data(); + let config_data = self.config.tree.data_for_specifier(&referrer); let mut roots = if !specifiers.is_empty() { specifiers } else { @@ -3451,16 +3457,17 @@ impl Inner { // always include the npm packages since resolution of one npm package // might affect the resolution of other npm packages - roots.extend( - self - .documents - .npm_reqs_by_scope() - .values() - .flatten() - .collect::>() - .iter() - .map(|req| ModuleSpecifier::parse(&format!("npm:{}", req)).unwrap()), - ); + if let Some(npm_reqs) = self + .documents + .npm_reqs_by_scope() + .get(&config_data.map(|d| d.scope.clone())) + { + roots.extend( + npm_reqs + .iter() + .map(|req| ModuleSpecifier::parse(&format!("npm:{}", req)).unwrap()), + ); + } let workspace_settings = self.config.workspace_settings(); let cli_options = CliOptions::new( diff --git a/cli/lsp/resolver.rs b/cli/lsp/resolver.rs index 9790dfed71..c4c66f1144 100644 --- a/cli/lsp/resolver.rs +++ b/cli/lsp/resolver.rs @@ -7,6 +7,7 @@ use crate::graph_util::CliJsrUrlProvider; use crate::http_util::HttpClientProvider; use crate::lsp::config::Config; use crate::lsp::config::ConfigData; +use crate::lsp::logging::lsp_warn; use crate::npm::create_cli_npm_resolver_for_lsp; use crate::npm::CliNpmResolver; use crate::npm::CliNpmResolverByonmCreateOptions; @@ -54,17 +55,17 @@ use super::cache::LspCache; use super::jsr::JsrCacheResolver; #[derive(Debug, Clone)] -pub struct LspResolver { +struct LspScopeResolver { graph_resolver: Arc, jsr_resolver: Option>, npm_resolver: Option>, node_resolver: Option>, redirect_resolver: Option>, graph_imports: Arc>, - config: Arc, + config_data: Option>, } -impl Default for LspResolver { +impl Default for LspScopeResolver { fn default() -> Self { Self { graph_resolver: create_graph_resolver(None, None, None), @@ -73,38 +74,41 @@ impl Default for LspResolver { node_resolver: None, redirect_resolver: None, graph_imports: Default::default(), - config: Default::default(), + config_data: None, } } } -impl LspResolver { - pub async fn from_config( +impl LspScopeResolver { + async fn from_config_data( + config_data: Option<&Arc>, config: &Config, cache: &LspCache, http_client_provider: Option<&Arc>, ) -> Self { - let config_data = config.tree.root_data(); let mut npm_resolver = None; let mut node_resolver = None; - if let (Some(http_client), Some(config_data)) = - (http_client_provider, config_data) - { - npm_resolver = create_npm_resolver(config_data, cache, http_client).await; + if let Some(http_client) = http_client_provider { + npm_resolver = create_npm_resolver( + config_data.map(|d| d.as_ref()), + cache, + http_client, + ) + .await; node_resolver = create_node_resolver(npm_resolver.as_ref()); } let graph_resolver = create_graph_resolver( - config_data, + config_data.map(|d| d.as_ref()), npm_resolver.as_ref(), node_resolver.as_ref(), ); let jsr_resolver = Some(Arc::new(JsrCacheResolver::new( - cache.root_vendor_or_global(), - config_data, + cache.for_specifier(config_data.map(|d| &d.scope)), + config_data.map(|d| d.as_ref()), config, ))); let redirect_resolver = Some(Arc::new(RedirectResolver::new( - cache.root_vendor_or_global(), + cache.for_specifier(config_data.map(|d| &d.scope)), ))); let npm_graph_resolver = graph_resolver.create_graph_npm_resolver(); let graph_imports = config_data @@ -135,16 +139,16 @@ impl LspResolver { node_resolver, redirect_resolver, graph_imports, - config: Arc::new(config.clone()), + config_data: config_data.cloned(), } } - pub fn snapshot(&self) -> Arc { + fn snapshot(&self) -> Arc { let npm_resolver = self.npm_resolver.as_ref().map(|r| r.clone_snapshotted()); let node_resolver = create_node_resolver(npm_resolver.as_ref()); let graph_resolver = create_graph_resolver( - self.config.tree.root_data(), + self.config_data.as_deref(), npm_resolver.as_ref(), node_resolver.as_ref(), ); @@ -155,68 +159,133 @@ impl LspResolver { node_resolver, redirect_resolver: self.redirect_resolver.clone(), graph_imports: self.graph_imports.clone(), - config: self.config.clone(), + config_data: self.config_data.clone(), + }) + } +} + +#[derive(Debug, Default, Clone)] +pub struct LspResolver { + unscoped: Arc, + by_scope: BTreeMap>, +} + +impl LspResolver { + pub async fn from_config( + config: &Config, + cache: &LspCache, + http_client_provider: Option<&Arc>, + ) -> Self { + let mut by_scope = BTreeMap::new(); + for (scope, config_data) in config.tree.data_by_scope().as_ref() { + by_scope.insert( + scope.clone(), + Arc::new( + LspScopeResolver::from_config_data( + Some(config_data), + config, + cache, + http_client_provider, + ) + .await, + ), + ); + } + Self { + unscoped: Arc::new( + LspScopeResolver::from_config_data( + None, + config, + cache, + http_client_provider, + ) + .await, + ), + by_scope, + } + } + + pub fn snapshot(&self) -> Arc { + Arc::new(Self { + unscoped: self.unscoped.snapshot(), + by_scope: self + .by_scope + .iter() + .map(|(s, r)| (s.clone(), r.snapshot())) + .collect(), }) } pub fn did_cache(&self) { - self.jsr_resolver.as_ref().inspect(|r| r.did_cache()); + for resolver in + std::iter::once(&self.unscoped).chain(self.by_scope.values()) + { + resolver.jsr_resolver.as_ref().inspect(|r| r.did_cache()); + } } pub async fn set_npm_reqs( &self, reqs: &BTreeMap, BTreeSet>, - ) -> Result<(), AnyError> { - let reqs = reqs - .values() - .flatten() - .collect::>() + ) { + for (scope, resolver) in [(None, &self.unscoped)] .into_iter() - .cloned() - .collect::>(); - if let Some(npm_resolver) = self.npm_resolver.as_ref() { - if let Some(npm_resolver) = npm_resolver.as_managed() { - return npm_resolver.set_package_reqs(&reqs).await; + .chain(self.by_scope.iter().map(|(s, r)| (Some(s), r))) + { + if let Some(npm_resolver) = resolver.npm_resolver.as_ref() { + if let Some(npm_resolver) = npm_resolver.as_managed() { + let reqs = reqs + .get(&scope.cloned()) + .map(|reqs| reqs.iter().cloned().collect::>()) + .unwrap_or_default(); + if let Err(err) = npm_resolver.set_package_reqs(&reqs).await { + lsp_warn!("Could not set npm package requirements: {:#}", err); + } + } } } - Ok(()) } pub fn as_graph_resolver( &self, - _file_referrer: Option<&ModuleSpecifier>, + file_referrer: Option<&ModuleSpecifier>, ) -> &dyn Resolver { - self.graph_resolver.as_ref() + let resolver = self.get_scope_resolver(file_referrer); + resolver.graph_resolver.as_ref() } pub fn create_graph_npm_resolver( &self, - _file_referrer: Option<&ModuleSpecifier>, + file_referrer: Option<&ModuleSpecifier>, ) -> WorkerCliNpmGraphResolver { - self.graph_resolver.create_graph_npm_resolver() + let resolver = self.get_scope_resolver(file_referrer); + resolver.graph_resolver.create_graph_npm_resolver() } pub fn maybe_managed_npm_resolver( &self, - _file_referrer: Option<&ModuleSpecifier>, + file_referrer: Option<&ModuleSpecifier>, ) -> Option<&ManagedCliNpmResolver> { - self.npm_resolver.as_ref().and_then(|r| r.as_managed()) + let resolver = self.get_scope_resolver(file_referrer); + resolver.npm_resolver.as_ref().and_then(|r| r.as_managed()) } pub fn graph_imports_by_referrer( &self, ) -> IndexMap<&ModuleSpecifier, Vec<&ModuleSpecifier>> { self - .graph_imports + .by_scope .iter() - .map(|(s, i)| { - ( - s, - i.dependencies - .values() - .flat_map(|d| d.get_type().or_else(|| d.get_code())) - .collect(), - ) + .flat_map(|(_, r)| { + r.graph_imports.iter().map(|(s, i)| { + ( + s, + i.dependencies + .values() + .flat_map(|d| d.get_type().or_else(|| d.get_code())) + .collect(), + ) + }) }) .collect() } @@ -224,35 +293,42 @@ impl LspResolver { pub fn jsr_to_resource_url( &self, req_ref: &JsrPackageReqReference, - _file_referrer: Option<&ModuleSpecifier>, + file_referrer: Option<&ModuleSpecifier>, ) -> Option { - self.jsr_resolver.as_ref()?.jsr_to_resource_url(req_ref) + let resolver = self.get_scope_resolver(file_referrer); + resolver.jsr_resolver.as_ref()?.jsr_to_resource_url(req_ref) } pub fn jsr_lookup_export_for_path( &self, nv: &PackageNv, path: &str, - _file_referrer: Option<&ModuleSpecifier>, + file_referrer: Option<&ModuleSpecifier>, ) -> Option { - self.jsr_resolver.as_ref()?.lookup_export_for_path(nv, path) + let resolver = self.get_scope_resolver(file_referrer); + resolver + .jsr_resolver + .as_ref()? + .lookup_export_for_path(nv, path) } pub fn jsr_lookup_req_for_nv( &self, nv: &PackageNv, - _file_referrer: Option<&ModuleSpecifier>, + file_referrer: Option<&ModuleSpecifier>, ) -> Option { - self.jsr_resolver.as_ref()?.lookup_req_for_nv(nv) + let resolver = self.get_scope_resolver(file_referrer); + resolver.jsr_resolver.as_ref()?.lookup_req_for_nv(nv) } pub fn npm_to_file_url( &self, req_ref: &NpmPackageReqReference, referrer: &ModuleSpecifier, - _file_referrer: Option<&ModuleSpecifier>, + file_referrer: Option<&ModuleSpecifier>, ) -> Option<(ModuleSpecifier, MediaType)> { - let node_resolver = self.node_resolver.as_ref()?; + let resolver = self.get_scope_resolver(file_referrer); + let node_resolver = resolver.node_resolver.as_ref()?; Some(NodeResolution::into_specifier_and_media_type( node_resolver .resolve_req_reference(req_ref, referrer, NodeResolutionMode::Types) @@ -261,7 +337,8 @@ impl LspResolver { } pub fn in_node_modules(&self, specifier: &ModuleSpecifier) -> bool { - if let Some(npm_resolver) = &self.npm_resolver { + let resolver = self.get_scope_resolver(Some(specifier)); + if let Some(npm_resolver) = &resolver.npm_resolver { return npm_resolver.in_npm_package(specifier); } false @@ -271,7 +348,8 @@ impl LspResolver { &self, specifier: &ModuleSpecifier, ) -> Option { - let node_resolver = self.node_resolver.as_ref()?; + let resolver = self.get_scope_resolver(Some(specifier)); + let node_resolver = resolver.node_resolver.as_ref()?; let resolution = node_resolver .url_to_node_resolution(specifier.clone()) .ok()?; @@ -282,7 +360,8 @@ impl LspResolver { &self, referrer: &ModuleSpecifier, ) -> Result>, AnyError> { - let Some(node_resolver) = self.node_resolver.as_ref() else { + let resolver = self.get_scope_resolver(Some(referrer)); + let Some(node_resolver) = resolver.node_resolver.as_ref() else { return Ok(None); }; node_resolver.get_closest_package_json( @@ -294,9 +373,10 @@ impl LspResolver { pub fn resolve_redirects( &self, specifier: &ModuleSpecifier, - _file_referrer: Option<&ModuleSpecifier>, + file_referrer: Option<&ModuleSpecifier>, ) -> Option { - let Some(redirect_resolver) = self.redirect_resolver.as_ref() else { + let resolver = self.get_scope_resolver(file_referrer); + let Some(redirect_resolver) = resolver.redirect_resolver.as_ref() else { return Some(specifier.clone()); }; redirect_resolver.resolve(specifier) @@ -305,9 +385,10 @@ impl LspResolver { pub fn redirect_chain_headers( &self, specifier: &ModuleSpecifier, - _file_referrer: Option<&ModuleSpecifier>, + file_referrer: Option<&ModuleSpecifier>, ) -> Vec<(ModuleSpecifier, Arc>)> { - let Some(redirect_resolver) = self.redirect_resolver.as_ref() else { + let resolver = self.get_scope_resolver(file_referrer); + let Some(redirect_resolver) = resolver.redirect_resolver.as_ref() else { return vec![]; }; redirect_resolver @@ -316,26 +397,47 @@ impl LspResolver { .map(|(s, e)| (s, e.headers.clone())) .collect() } + + fn get_scope_resolver( + &self, + file_referrer: Option<&ModuleSpecifier>, + ) -> &LspScopeResolver { + let Some(file_referrer) = file_referrer else { + return self.unscoped.as_ref(); + }; + self + .by_scope + .iter() + .rfind(|(s, _)| file_referrer.as_str().starts_with(s.as_str())) + .map(|(_, r)| r.as_ref()) + .unwrap_or(self.unscoped.as_ref()) + } } async fn create_npm_resolver( - config_data: &ConfigData, + config_data: Option<&ConfigData>, cache: &LspCache, http_client_provider: &Arc, ) -> Option> { - let node_modules_dir = config_data - .node_modules_dir - .clone() - .or_else(|| specifier_to_file_path(&config_data.scope).ok())?; - let options = if config_data.byonm { + let mut byonm_dir = None; + if let Some(config_data) = config_data { + if config_data.byonm { + byonm_dir = Some(config_data.node_modules_dir.clone().or_else(|| { + specifier_to_file_path(&config_data.scope) + .ok() + .map(|p| p.join("node_modules/")) + })?) + } + } + let options = if let Some(byonm_dir) = byonm_dir { CliNpmResolverCreateOptions::Byonm(CliNpmResolverByonmCreateOptions { fs: Arc::new(deno_fs::RealFs), - root_node_modules_dir: node_modules_dir.clone(), + root_node_modules_dir: byonm_dir, }) } else { CliNpmResolverCreateOptions::Managed(CliNpmResolverManagedCreateOptions { http_client_provider: http_client_provider.clone(), - snapshot: match config_data.lockfile.as_ref() { + snapshot: match config_data.and_then(|d| d.lockfile.as_ref()) { Some(lockfile) => { CliNpmResolverManagedSnapshotOption::ResolveFromLockfile( lockfile.clone(), @@ -354,15 +456,17 @@ async fn create_npm_resolver( // the user is typing. cache_setting: CacheSetting::Only, text_only_progress_bar: ProgressBar::new(ProgressBarStyle::TextOnly), - maybe_node_modules_path: config_data.node_modules_dir.clone(), + maybe_node_modules_path: config_data + .and_then(|d| d.node_modules_dir.clone()), package_json_deps_provider: Arc::new(PackageJsonDepsProvider::new( - config_data.package_json.as_ref().map(|package_json| { - package_json::get_local_package_json_version_reqs(package_json) - }), + config_data + .and_then(|d| d.package_json.as_ref()) + .map(|package_json| { + package_json::get_local_package_json_version_reqs(package_json) + }), )), npmrc: config_data - .npmrc - .clone() + .and_then(|d| d.npmrc.clone()) .unwrap_or_else(create_default_npmrc), npm_system_info: NpmSystemInfo::default(), }) diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 5e5a509ac2..5659decbf9 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -19,7 +19,6 @@ use super::semantic_tokens; use super::semantic_tokens::SemanticTokensBuilder; use super::text::LineIndex; use super::urls::LspClientUrl; -use super::urls::LspUrlMap; use super::urls::INVALID_SPECIFIER; use crate::args::jsr_url; @@ -1844,9 +1843,12 @@ impl DocumentSpan { let target_asset_or_doc = language_server.get_maybe_asset_or_document(&target_specifier)?; let target_line_index = target_asset_or_doc.line_index(); + let file_referrer = language_server + .documents + .get_file_referrer(&target_specifier); let target_uri = language_server .url_map - .normalize_specifier(&target_specifier) + .normalize_specifier(&target_specifier, file_referrer.as_deref()) .ok()?; let (target_range, target_selection_range) = if let Some(context_span) = &self.context_span { @@ -1890,9 +1892,10 @@ impl DocumentSpan { language_server.get_maybe_asset_or_document(&specifier)?; let line_index = asset_or_doc.line_index(); let range = self.text_span.to_range(line_index); + let file_referrer = language_server.documents.get_file_referrer(&specifier); let mut target = language_server .url_map - .normalize_specifier(&specifier) + .normalize_specifier(&specifier, file_referrer.as_deref()) .ok()? .into_url(); target.set_fragment(Some(&format!( @@ -1950,9 +1953,10 @@ impl NavigateToItem { let asset_or_doc = language_server.get_asset_or_document(&specifier).ok()?; let line_index = asset_or_doc.line_index(); + let file_referrer = language_server.documents.get_file_referrer(&specifier); let uri = language_server .url_map - .normalize_specifier(&specifier) + .normalize_specifier(&specifier, file_referrer.as_deref()) .ok()?; let range = self.text_span.to_range(line_index); let location = lsp::Location { @@ -2208,9 +2212,10 @@ impl ImplementationLocation { ) -> lsp::Location { let specifier = resolve_url(&self.document_span.file_name) .unwrap_or_else(|_| ModuleSpecifier::parse("deno://invalid").unwrap()); + let file_referrer = language_server.documents.get_file_referrer(&specifier); let uri = language_server .url_map - .normalize_specifier(&specifier) + .normalize_specifier(&specifier, file_referrer.as_deref()) .unwrap_or_else(|_| { LspClientUrl::new(ModuleSpecifier::parse("deno://invalid").unwrap()) }); @@ -2270,7 +2275,11 @@ impl RenameLocations { includes_non_files = true; continue; } - let uri = language_server.url_map.normalize_specifier(&specifier)?; + let file_referrer = + language_server.documents.get_file_referrer(&specifier); + let uri = language_server + .url_map + .normalize_specifier(&specifier, file_referrer.as_deref())?; let asset_or_doc = language_server.get_asset_or_document(&specifier)?; // ensure TextDocumentEdit for `location.file_name`. @@ -2916,12 +2925,14 @@ impl ReferenceEntry { pub fn to_location( &self, line_index: Arc, - url_map: &LspUrlMap, + language_server: &language_server::Inner, ) -> lsp::Location { let specifier = resolve_url(&self.document_span.file_name) .unwrap_or_else(|_| INVALID_SPECIFIER.clone()); - let uri = url_map - .normalize_specifier(&specifier) + let file_referrer = language_server.documents.get_file_referrer(&specifier); + let uri = language_server + .url_map + .normalize_specifier(&specifier, file_referrer.as_deref()) .unwrap_or_else(|_| LspClientUrl::new(INVALID_SPECIFIER.clone())); lsp::Location { uri: uri.into_url(), @@ -2977,9 +2988,12 @@ impl CallHierarchyItem { ) -> lsp::CallHierarchyItem { let target_specifier = resolve_url(&self.file).unwrap_or_else(|_| INVALID_SPECIFIER.clone()); + let file_referrer = language_server + .documents + .get_file_referrer(&target_specifier); let uri = language_server .url_map - .normalize_specifier(&target_specifier) + .normalize_specifier(&target_specifier, file_referrer.as_deref()) .unwrap_or_else(|_| LspClientUrl::new(INVALID_SPECIFIER.clone())); let use_file_name = self.is_source_file_item(); diff --git a/cli/lsp/urls.rs b/cli/lsp/urls.rs index f1e75c9ddc..594c223b43 100644 --- a/cli/lsp/urls.rs +++ b/cli/lsp/urls.rs @@ -1,7 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use crate::cache::LocalLspHttpCache; - use deno_ast::MediaType; use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; @@ -12,6 +10,8 @@ use once_cell::sync::Lazy; use std::collections::HashMap; use std::sync::Arc; +use super::cache::LspCache; + /// Used in situations where a default URL needs to be used where otherwise a /// panic is undesired. pub static INVALID_SPECIFIER: Lazy = @@ -156,13 +156,13 @@ pub enum LspUrlKind { /// to allow the Deno language server to manage these as virtual documents. #[derive(Debug, Default, Clone)] pub struct LspUrlMap { - local_http_cache: Option>, + cache: LspCache, inner: Arc>, } impl LspUrlMap { - pub fn set_cache(&mut self, http_cache: Option>) { - self.local_http_cache = http_cache; + 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 @@ -170,13 +170,12 @@ impl LspUrlMap { pub fn normalize_specifier( &self, specifier: &ModuleSpecifier, + file_referrer: Option<&ModuleSpecifier>, ) -> Result { - if let Some(cache) = &self.local_http_cache { - if matches!(specifier.scheme(), "http" | "https") { - if let Some(file_url) = cache.get_file_url(specifier) { - return Ok(LspClientUrl(file_url)); - } - } + if let Some(file_url) = + self.cache.vendored_specifier(specifier, file_referrer) + { + return Ok(LspClientUrl(file_url)); } let mut inner = self.inner.lock(); if let Some(url) = inner.get_url(specifier).cloned() { @@ -220,14 +219,8 @@ impl LspUrlMap { /// 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 normalize_url(&self, url: &Url, kind: LspUrlKind) -> ModuleSpecifier { - if let Some(cache) = &self.local_http_cache { - if url.scheme() == "file" { - if let Ok(path) = url.to_file_path() { - if let Some(remote_url) = cache.get_remote_url(&path) { - return remote_url; - } - } - } + 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(url).cloned() { @@ -296,7 +289,7 @@ mod tests { let map = LspUrlMap::default(); let fixture = resolve_url("https://deno.land/x/pkg@1.0.0/mod.ts").unwrap(); let actual_url = map - .normalize_specifier(&fixture) + .normalize_specifier(&fixture, None) .expect("could not handle specifier"); let expected_url = Url::parse("deno:/https/deno.land/x/pkg%401.0.0/mod.ts").unwrap(); @@ -318,7 +311,7 @@ mod tests { assert_eq!(&actual_specifier, &expected_specifier); let actual_url = map - .normalize_specifier(&actual_specifier) + .normalize_specifier(&actual_specifier, None) .unwrap() .as_url() .clone(); @@ -331,7 +324,7 @@ mod tests { 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_url = map - .normalize_specifier(&fixture) + .normalize_specifier(&fixture, None) .expect("could not handle specifier"); let expected_url = Url::parse("deno:/https/cdn.skypack.dev/-/postcss%40v8.2.9-E4SktPp9c0AtxrJHp8iV/dist%3Des2020%2Cmode%3Dtypes/lib/postcss.d.ts").unwrap(); assert_eq!(actual_url.as_url(), &expected_url); @@ -346,7 +339,7 @@ mod tests { let map = LspUrlMap::default(); let fixture = resolve_url("data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=").unwrap(); let actual_url = map - .normalize_specifier(&fixture) + .normalize_specifier(&fixture, None) .expect("could not handle specifier"); let expected_url = Url::parse("deno:/c21c7fc382b2b0553dc0864aa81a3acacfb7b3d1285ab5ae76da6abec213fb37/data_url.ts").unwrap(); assert_eq!(actual_url.as_url(), &expected_url); @@ -361,7 +354,7 @@ mod tests { let map = LspUrlMap::default(); let fixture = resolve_url("http://localhost:8000/mod.ts").unwrap(); let actual_url = map - .normalize_specifier(&fixture) + .normalize_specifier(&fixture, None) .expect("could not handle specifier"); let expected_url = Url::parse("deno:/http/localhost%3A8000/mod.ts").unwrap(); diff --git a/tests/integration/lsp_tests.rs b/tests/integration/lsp_tests.rs index 554fc7ac15..3692bf7d96 100644 --- a/tests/integration/lsp_tests.rs +++ b/tests/integration/lsp_tests.rs @@ -280,13 +280,14 @@ fn lsp_import_map_remote() { #[test] fn lsp_import_map_data_url() { let context = TestContextBuilder::new().use_temp_cwd().build(); + let temp_dir = context.temp_dir(); let mut client = context.new_lsp_command().build(); client.initialize(|builder| { builder.set_import_map("data:application/json;utf8,{\"imports\": { \"example\": \"https://deno.land/x/example/mod.ts\" }}"); }); let diagnostics = client.did_open(json!({ "textDocument": { - "uri": "file:///a/file.ts", + "uri": temp_dir.uri().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import example from \"example\";\n" @@ -780,7 +781,7 @@ fn lsp_format_vendor_path() { client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { - "uri": "file:///a/file.ts", + "uri": temp_dir.uri().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#"import "http://localhost:4545/run/002_hello.ts";"#, @@ -802,7 +803,7 @@ fn lsp_format_vendor_path() { "workspace/executeCommand", json!({ "command": "deno.cache", - "arguments": [[], "file:///a/file.ts"], + "arguments": [[], temp_dir.uri().join("file.ts").unwrap()], }), ); assert!(temp_dir @@ -2622,7 +2623,7 @@ fn lsp_import_map_setting_with_deno_json() { }); let diagnostics = client.did_open(json!({ "textDocument": { - "uri": "file:///a/file.ts", + "uri": temp_dir.uri().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "import \"file2\";\n", @@ -7585,7 +7586,7 @@ fn lsp_completions_auto_import_and_quick_fix_with_import_map() { client.did_open( json!({ "textDocument": { - "uri": "file:///a/file.ts", + "uri": temp_dir.uri().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": concat!( @@ -7612,7 +7613,7 @@ fn lsp_completions_auto_import_and_quick_fix_with_import_map() { "npm:chalk@~5", "http://localhost:4545/subdir/print_hello.ts", ], - "file:///a/file.ts", + temp_dir.uri().join("file.ts").unwrap(), ], }), ); @@ -7620,14 +7621,14 @@ fn lsp_completions_auto_import_and_quick_fix_with_import_map() { // try auto-import with path client.did_open(json!({ "textDocument": { - "uri": "file:///a/a.ts", + "uri": temp_dir.uri().join("a.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "getClie", } })); let list = client.get_completion_list( - "file:///a/a.ts", + temp_dir.uri().join("a.ts").unwrap(), (0, 7), json!({ "triggerKind": 1 }), ); @@ -7668,20 +7669,23 @@ fn lsp_completions_auto_import_and_quick_fix_with_import_map() { // try quick fix with path let diagnostics = client.did_open(json!({ "textDocument": { - "uri": "file:///a/b.ts", + "uri": temp_dir.uri().join("b.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "getClient", } })); let diagnostics = diagnostics - .messages_with_file_and_source("file:///a/b.ts", "deno-ts") + .messages_with_file_and_source( + temp_dir.uri().join("b.ts").unwrap().as_str(), + "deno-ts", + ) .diagnostics; let res = client.write_request( "textDocument/codeAction", json!(json!({ "textDocument": { - "uri": "file:///a/b.ts" + "uri": temp_dir.uri().join("b.ts").unwrap() }, "range": { "start": { "line": 0, "character": 0 }, @@ -7713,7 +7717,7 @@ fn lsp_completions_auto_import_and_quick_fix_with_import_map() { "edit": { "documentChanges": [{ "textDocument": { - "uri": "file:///a/b.ts", + "uri": temp_dir.uri().join("b.ts").unwrap(), "version": 1, }, "edits": [{ @@ -7731,7 +7735,7 @@ fn lsp_completions_auto_import_and_quick_fix_with_import_map() { // try auto-import without path client.did_open(json!({ "textDocument": { - "uri": "file:///a/c.ts", + "uri": temp_dir.uri().join("c.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "chal", @@ -7739,7 +7743,7 @@ fn lsp_completions_auto_import_and_quick_fix_with_import_map() { })); let list = client.get_completion_list( - "file:///a/c.ts", + temp_dir.uri().join("c.ts").unwrap(), (0, 4), json!({ "triggerKind": 1 }), ); @@ -7778,20 +7782,23 @@ fn lsp_completions_auto_import_and_quick_fix_with_import_map() { // try quick fix without path let diagnostics = client.did_open(json!({ "textDocument": { - "uri": "file:///a/d.ts", + "uri": temp_dir.uri().join("d.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "chalk", } })); let diagnostics = diagnostics - .messages_with_file_and_source("file:///a/d.ts", "deno-ts") + .messages_with_file_and_source( + temp_dir.uri().join("d.ts").unwrap().as_str(), + "deno-ts", + ) .diagnostics; let res = client.write_request( "textDocument/codeAction", json!(json!({ "textDocument": { - "uri": "file:///a/d.ts" + "uri": temp_dir.uri().join("d.ts").unwrap() }, "range": { "start": { "line": 0, "character": 0 }, @@ -7823,7 +7830,7 @@ fn lsp_completions_auto_import_and_quick_fix_with_import_map() { "edit": { "documentChanges": [{ "textDocument": { - "uri": "file:///a/d.ts", + "uri": temp_dir.uri().join("d.ts").unwrap(), "version": 1, }, "edits": [{ @@ -7841,7 +7848,7 @@ fn lsp_completions_auto_import_and_quick_fix_with_import_map() { // try auto-import with http import map client.did_open(json!({ "textDocument": { - "uri": "file:///a/e.ts", + "uri": temp_dir.uri().join("e.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "printH", @@ -7849,7 +7856,7 @@ fn lsp_completions_auto_import_and_quick_fix_with_import_map() { })); let list = client.get_completion_list( - "file:///a/e.ts", + temp_dir.uri().join("e.ts").unwrap(), (0, 6), json!({ "triggerKind": 1 }), ); @@ -7888,20 +7895,23 @@ fn lsp_completions_auto_import_and_quick_fix_with_import_map() { // try quick fix with http import let diagnostics = client.did_open(json!({ "textDocument": { - "uri": "file:///a/f.ts", + "uri": temp_dir.uri().join("f.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "printHello", } })); let diagnostics = diagnostics - .messages_with_file_and_source("file:///a/f.ts", "deno-ts") + .messages_with_file_and_source( + temp_dir.uri().join("f.ts").unwrap().as_str(), + "deno-ts", + ) .diagnostics; let res = client.write_request( "textDocument/codeAction", json!(json!({ "textDocument": { - "uri": "file:///a/f.ts" + "uri": temp_dir.uri().join("f.ts").unwrap() }, "range": { "start": { "line": 0, "character": 0 }, @@ -7933,7 +7943,7 @@ fn lsp_completions_auto_import_and_quick_fix_with_import_map() { "edit": { "documentChanges": [{ "textDocument": { - "uri": "file:///a/f.ts", + "uri": temp_dir.uri().join("f.ts").unwrap(), "version": 1, }, "edits": [{ @@ -7951,14 +7961,14 @@ fn lsp_completions_auto_import_and_quick_fix_with_import_map() { // try auto-import with npm package with sub-path on value side of import map client.did_open(json!({ "textDocument": { - "uri": "file:///a/nested_path.ts", + "uri": temp_dir.uri().join("nested_path.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "entry", } })); let list = client.get_completion_list( - "file:///a/nested_path.ts", + temp_dir.uri().join("nested_path.ts").unwrap(), (0, 5), json!({ "triggerKind": 1 }), ); @@ -11001,7 +11011,7 @@ fn lsp_lint_with_config() { let diagnostics = client.did_open(json!({ "textDocument": { - "uri": "file:///a/file.ts", + "uri": temp_dir.uri().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "// TODO: fixme\nexport async function non_camel_case() {\nconsole.log(\"finished!\")\n}" @@ -12104,6 +12114,323 @@ fn lsp_vendor_dir() { client.shutdown(); } + +#[test] +fn lsp_deno_json_scopes_import_map() { + let context = TestContextBuilder::new().use_temp_cwd().build(); + let temp_dir = context.temp_dir(); + temp_dir.create_dir_all("project1"); + temp_dir.create_dir_all("project2/project3"); + temp_dir.write( + "project1/deno.json", + json!({ + "imports": { + "foo": "./foo1.ts", + }, + }) + .to_string(), + ); + temp_dir.write("project1/foo1.ts", ""); + temp_dir.write( + "project2/deno.json", + json!({ + "imports": { + "foo": "./foo2.ts", + }, + }) + .to_string(), + ); + temp_dir.write("project2/foo2.ts", ""); + temp_dir.write( + "project2/project3/deno.json", + json!({ + "imports": { + "foo": "./foo3.ts", + }, + }) + .to_string(), + ); + temp_dir.write("project2/project3/foo3.ts", ""); + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + client.did_open(json!({ + "textDocument": { + "uri": temp_dir.uri().join("project1/file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": "import \"foo\";\n", + }, + })); + let res = client.write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": temp_dir.uri().join("project1/file.ts").unwrap(), + }, + "position": { "line": 0, "character": 7 }, + }), + ); + assert_eq!( + res, + json!({ + "contents": { + "kind": "markdown", + "value": format!("**Resolved Dependency**\n\n**Code**: file​{}\n", temp_dir.uri().join("project1/foo1.ts").unwrap().as_str().trim_start_matches("file")), + }, + "range": { + "start": { "line": 0, "character": 7 }, + "end": { "line": 0, "character": 12 }, + }, + }) + ); + client.did_open(json!({ + "textDocument": { + "uri": temp_dir.uri().join("project2/file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": "import \"foo\";\n", + }, + })); + let res = client.write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": temp_dir.uri().join("project2/file.ts").unwrap(), + }, + "position": { "line": 0, "character": 7 }, + }), + ); + assert_eq!( + res, + json!({ + "contents": { + "kind": "markdown", + "value": format!("**Resolved Dependency**\n\n**Code**: file​{}\n", temp_dir.uri().join("project2/foo2.ts").unwrap().as_str().trim_start_matches("file")), + }, + "range": { + "start": { "line": 0, "character": 7 }, + "end": { "line": 0, "character": 12 }, + }, + }) + ); + client.did_open(json!({ + "textDocument": { + "uri": temp_dir.uri().join("project2/project3/file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": "import \"foo\";\n", + }, + })); + let res = client.write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": temp_dir.uri().join("project2/project3/file.ts").unwrap(), + }, + "position": { "line": 0, "character": 7 }, + }), + ); + assert_eq!( + res, + json!({ + "contents": { + "kind": "markdown", + "value": format!("**Resolved Dependency**\n\n**Code**: file​{}\n", temp_dir.uri().join("project2/project3/foo3.ts").unwrap().as_str().trim_start_matches("file")), + }, + "range": { + "start": { "line": 0, "character": 7 }, + "end": { "line": 0, "character": 12 }, + }, + }) + ); + client.shutdown(); +} + +#[test] +fn lsp_deno_json_scopes_vendor_dirs() { + let context = TestContextBuilder::new() + .use_http_server() + .use_temp_cwd() + .build(); + let temp_dir = context.temp_dir(); + temp_dir.create_dir_all("project1"); + temp_dir.create_dir_all("project2/project3"); + temp_dir.write( + "project1/deno.json", + json!({ + "vendor": true, + }) + .to_string(), + ); + temp_dir.write( + "project2/deno.json", + json!({ + "vendor": true, + }) + .to_string(), + ); + temp_dir.write( + "project2/project3/deno.json", + json!({ + "vendor": true, + }) + .to_string(), + ); + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + client.did_open(json!({ + "textDocument": { + "uri": temp_dir.uri().join("project1/file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": "import \"http://localhost:4545/subdir/mod1.ts\";\n", + }, + })); + client.write_request( + "workspace/executeCommand", + json!({ + "command": "deno.cache", + "arguments": [[], temp_dir.uri().join("project1/file.ts").unwrap()], + }), + ); + let res = client.write_request( + "textDocument/definition", + json!({ + "textDocument": { + "uri": temp_dir.uri().join("project1/file.ts").unwrap(), + }, + "position": { "line": 0, "character": 7 }, + }), + ); + assert_eq!( + res, + json!([{ + "targetUri": temp_dir.uri().join("project1/vendor/http_localhost_4545/subdir/mod1.ts").unwrap(), + "targetRange": { + "start": { + "line": 0, + "character": 0, + }, + "end": { + "line": 17, + "character": 0, + }, + }, + "targetSelectionRange": { + "start": { + "line": 0, + "character": 0, + }, + "end": { + "line": 17, + "character": 0, + }, + }, + }]), + ); + client.did_open(json!({ + "textDocument": { + "uri": temp_dir.uri().join("project2/file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": "import \"http://localhost:4545/subdir/mod2.ts\";\n", + }, + })); + client.write_request( + "workspace/executeCommand", + json!({ + "command": "deno.cache", + "arguments": [[], temp_dir.uri().join("project2/file.ts").unwrap()], + }), + ); + let res = client.write_request( + "textDocument/definition", + json!({ + "textDocument": { + "uri": temp_dir.uri().join("project2/file.ts").unwrap(), + }, + "position": { "line": 0, "character": 7 }, + }), + ); + assert_eq!( + res, + json!([{ + "targetUri": temp_dir.uri().join("project2/vendor/http_localhost_4545/subdir/mod2.ts").unwrap(), + "targetRange": { + "start": { + "line": 0, + "character": 0, + }, + "end": { + "line": 1, + "character": 0, + }, + }, + "targetSelectionRange": { + "start": { + "line": 0, + "character": 0, + }, + "end": { + "line": 1, + "character": 0, + }, + }, + }]), + ); + client.did_open(json!({ + "textDocument": { + "uri": temp_dir.uri().join("project2/project3/file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": "import \"http://localhost:4545/subdir/mod3.js\";\n", + }, + })); + client.write_request( + "workspace/executeCommand", + json!({ + "command": "deno.cache", + "arguments": [[], temp_dir.uri().join("project2/project3/file.ts").unwrap()], + }), + ); + let res = client.write_request( + "textDocument/definition", + json!({ + "textDocument": { + "uri": temp_dir.uri().join("project2/project3/file.ts").unwrap(), + }, + "position": { "line": 0, "character": 7 }, + }), + ); + assert_eq!( + res, + json!([{ + "targetUri": temp_dir.uri().join("project2/project3/vendor/http_localhost_4545/subdir/mod3.js").unwrap(), + "targetRange": { + "start": { + "line": 0, + "character": 0, + }, + "end": { + "line": 1, + "character": 0, + }, + }, + "targetSelectionRange": { + "start": { + "line": 0, + "character": 0, + }, + "end": { + "line": 1, + "character": 0, + }, + }, + }]), + ); + client.shutdown(); +} + #[test] fn lsp_deno_json_workspace_fmt_config() { let context = TestContextBuilder::new().use_temp_cwd().build(); @@ -13005,7 +13332,7 @@ fn lsp_uses_lockfile_for_npm_initialization() { assert!(!line.contains("Running npm resolution."), "Line: {}", line); line.contains("Server ready.") }); - assert_eq!(skipping_count, 1); + assert_eq!(skipping_count, 2); client.shutdown(); }