From 7c5dbd5d54770dba5e56442b633e9597403ef5da Mon Sep 17 00:00:00 2001 From: Nayeem Rahman Date: Mon, 10 Jun 2024 17:03:17 +0100 Subject: [PATCH] feat(lsp): workspace jsr resolution (#24121) --- cli/jsr.rs | 219 +-------------------------- cli/lsp/config.rs | 163 ++++++++++++++++---- cli/lsp/documents.rs | 2 +- cli/lsp/jsr.rs | 267 +++++++++++++++++++++++++++++++++ cli/lsp/language_server.rs | 2 +- cli/lsp/resolver.rs | 9 +- tests/integration/lsp_tests.rs | 189 +++++++++++++++-------- 7 files changed, 531 insertions(+), 320 deletions(-) diff --git a/cli/jsr.rs b/cli/jsr.rs index e582ab9f0e..87a54af22b 100644 --- a/cli/jsr.rs +++ b/cli/jsr.rs @@ -3,207 +3,14 @@ use crate::args::jsr_url; use crate::file_fetcher::FileFetcher; use dashmap::DashMap; -use deno_cache_dir::HttpCache; -use deno_core::parking_lot::Mutex; use deno_core::serde_json; -use deno_core::ModuleSpecifier; use deno_graph::packages::JsrPackageInfo; use deno_graph::packages::JsrPackageVersionInfo; -use deno_lockfile::Lockfile; use deno_runtime::deno_permissions::PermissionsContainer; -use deno_semver::jsr::JsrPackageReqReference; use deno_semver::package::PackageNv; use deno_semver::package::PackageReq; -use std::borrow::Cow; use std::sync::Arc; -/// Keep in sync with `JsrFetchResolver`! -#[derive(Debug)] -pub struct JsrCacheResolver { - nv_by_req: DashMap>, - /// The `module_graph` field of the version infos should be forcibly absent. - /// It can be large and we don't want to store it. - info_by_nv: DashMap>>, - info_by_name: DashMap>>, - cache: Arc, -} - -impl JsrCacheResolver { - pub fn new( - cache: Arc, - lockfile: Option>>, - ) -> Self { - let nv_by_req = DashMap::new(); - if let Some(lockfile) = lockfile { - for (req_url, nv_url) in &lockfile.lock().content.packages.specifiers { - let Some(req) = req_url.strip_prefix("jsr:") else { - continue; - }; - let Some(nv) = nv_url.strip_prefix("jsr:") else { - continue; - }; - let Ok(req) = PackageReq::from_str(req) else { - continue; - }; - let Ok(nv) = PackageNv::from_str(nv) else { - continue; - }; - nv_by_req.insert(req, Some(nv)); - } - } - Self { - nv_by_req, - info_by_nv: Default::default(), - info_by_name: Default::default(), - cache: cache.clone(), - } - } - - pub fn req_to_nv(&self, req: &PackageReq) -> Option { - if let Some(nv) = self.nv_by_req.get(req) { - return nv.value().clone(); - } - let maybe_get_nv = || { - let name = req.name.clone(); - let package_info = self.package_info(&name)?; - // Find the first matching version of the package which is cached. - let mut versions = package_info.versions.keys().collect::>(); - versions.sort(); - let version = versions - .into_iter() - .rev() - .find(|v| { - if req.version_req.tag().is_some() || !req.version_req.matches(v) { - return false; - } - let nv = PackageNv { - name: name.clone(), - version: (*v).clone(), - }; - self.package_version_info(&nv).is_some() - }) - .cloned()?; - Some(PackageNv { name, version }) - }; - let nv = maybe_get_nv(); - self.nv_by_req.insert(req.clone(), nv.clone()); - nv - } - - pub fn jsr_to_registry_url( - &self, - req_ref: &JsrPackageReqReference, - ) -> Option { - let req = req_ref.req().clone(); - let maybe_nv = self.req_to_nv(&req); - let nv = maybe_nv.as_ref()?; - let info = self.package_version_info(nv)?; - let path = info.export(&normalize_export_name(req_ref.sub_path()))?; - jsr_url() - .join(&format!("{}/{}/{}", &nv.name, &nv.version, &path)) - .ok() - } - - pub fn lookup_export_for_path( - &self, - nv: &PackageNv, - path: &str, - ) -> Option { - let info = self.package_version_info(nv)?; - let path = path.strip_prefix("./").unwrap_or(path); - let mut sloppy_fallback = None; - for (export, path_) in info.exports() { - let path_ = path_.strip_prefix("./").unwrap_or(path_); - if path_ == path { - return Some(export.strip_prefix("./").unwrap_or(export).to_string()); - } - // TSC in some cases will suggest a `.js` import path for a `.d.ts` source - // file. - if sloppy_fallback.is_none() { - let path = path - .strip_suffix(".js") - .or_else(|| path.strip_suffix(".mjs")) - .or_else(|| path.strip_suffix(".cjs")) - .unwrap_or(path); - let path_ = path_ - .strip_suffix(".d.ts") - .or_else(|| path_.strip_suffix(".d.mts")) - .or_else(|| path_.strip_suffix(".d.cts")) - .unwrap_or(path_); - if path_ == path { - sloppy_fallback = - Some(export.strip_prefix("./").unwrap_or(export).to_string()); - } - } - } - sloppy_fallback - } - - pub fn lookup_req_for_nv(&self, nv: &PackageNv) -> Option { - for entry in self.nv_by_req.iter() { - let Some(nv_) = entry.value() else { - continue; - }; - if nv_ == nv { - return Some(entry.key().clone()); - } - } - None - } - - pub fn package_info(&self, name: &str) -> Option> { - if let Some(info) = self.info_by_name.get(name) { - return info.value().clone(); - } - let read_cached_package_info = || { - let meta_url = jsr_url().join(&format!("{}/meta.json", name)).ok()?; - let meta_bytes = read_cached_url(&meta_url, &self.cache)?; - serde_json::from_slice::(&meta_bytes).ok() - }; - let info = read_cached_package_info().map(Arc::new); - self.info_by_name.insert(name.to_string(), info.clone()); - info - } - - pub fn package_version_info( - &self, - nv: &PackageNv, - ) -> Option> { - if let Some(info) = self.info_by_nv.get(nv) { - return info.value().clone(); - } - let read_cached_package_version_info = || { - let meta_url = jsr_url() - .join(&format!("{}/{}_meta.json", &nv.name, &nv.version)) - .ok()?; - let meta_bytes = read_cached_url(&meta_url, &self.cache)?; - partial_jsr_package_version_info_from_slice(&meta_bytes).ok() - }; - let info = read_cached_package_version_info().map(Arc::new); - self.info_by_nv.insert(nv.clone(), info.clone()); - info - } - - pub fn did_cache(&self) { - self.nv_by_req.retain(|_, nv| nv.is_some()); - self.info_by_nv.retain(|_, info| info.is_some()); - self.info_by_name.retain(|_, info| info.is_some()); - } -} - -fn read_cached_url( - url: &ModuleSpecifier, - cache: &Arc, -) -> Option> { - cache - .read_file_bytes( - &cache.cache_item_key(url).ok()?, - None, - deno_cache_dir::GlobalToLocalCopy::Disallow, - ) - .ok()? -} - /// This is similar to a subset of `JsrCacheResolver` which fetches rather than /// just reads the cache. Keep in sync! #[derive(Debug)] @@ -304,33 +111,9 @@ impl JsrFetchResolver { } } -// TODO(nayeemrmn): This is duplicated from a private function in deno_graph -// 0.65.1. Make it public or cleanup otherwise. -fn normalize_export_name(sub_path: Option<&str>) -> Cow { - let Some(sub_path) = sub_path else { - return Cow::Borrowed("."); - }; - if sub_path.is_empty() || matches!(sub_path, "/" | ".") { - Cow::Borrowed(".") - } else { - let sub_path = if sub_path.starts_with('/') { - Cow::Owned(format!(".{}", sub_path)) - } else if !sub_path.starts_with("./") { - Cow::Owned(format!("./{}", sub_path)) - } else { - Cow::Borrowed(sub_path) - }; - if let Some(prefix) = sub_path.strip_suffix('/') { - Cow::Owned(prefix.to_string()) - } else { - sub_path - } - } -} - /// This is a roundabout way of deserializing `JsrPackageVersionInfo`, /// because we only want the `exports` field and `module_graph` is large. -fn partial_jsr_package_version_info_from_slice( +pub fn partial_jsr_package_version_info_from_slice( slice: &[u8], ) -> serde_json::Result { let mut info = serde_json::from_slice::(slice)?; diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index f033888957..e445d34f03 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -17,6 +17,7 @@ use deno_ast::MediaType; use deno_config::FmtOptionsConfig; use deno_config::TsConfig; use deno_core::anyhow::anyhow; +use deno_core::normalize_path; use deno_core::parking_lot::Mutex; use deno_core::serde::de::DeserializeOwned; use deno_core::serde::Deserialize; @@ -31,6 +32,8 @@ use deno_npm::npm_rc::ResolvedNpmRc; use deno_runtime::deno_node::PackageJson; use deno_runtime::deno_permissions::PermissionsContainer; use deno_runtime::fs_util::specifier_to_file_path; +use deno_semver::package::PackageNv; +use deno_semver::Version; use import_map::ImportMap; use lsp::Url; use lsp_types::ClientCapabilities; @@ -1077,6 +1080,17 @@ impl LspTsConfig { } } +#[derive(Debug, Clone)] +pub struct LspWorkspaceConfig { + pub members: Vec, +} + +#[derive(Debug, Clone)] +pub struct LspPackageConfig { + pub nv: PackageNv, + pub exports: Value, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConfigWatchedFileType { DenoJson, @@ -1103,6 +1117,14 @@ pub struct ConfigData { pub npmrc: Option>, pub import_map: Option>, pub import_map_from_settings: bool, + pub package_config: Option>, + pub is_workspace_root: bool, + /// Workspace member directories. For a workspace root this will be a list of + /// members. For a member this will be the same list, representing self and + /// siblings. For a solitary package this will be `vec![self.scope]`. These + /// are the list of packages to override with local resolutions for this + /// config scope. + pub workspace_members: Arc>, watched_files: HashMap, } @@ -1110,7 +1132,7 @@ impl ConfigData { async fn load( config_file_specifier: Option<&ModuleSpecifier>, scope: &ModuleSpecifier, - parent: Option<(&ModuleSpecifier, &ConfigData)>, + workspace_root: Option<(&ModuleSpecifier, &ConfigData)>, settings: &Settings, file_fetcher: Option<&Arc>, ) -> Self { @@ -1127,7 +1149,7 @@ impl ConfigData { Self::load_inner( Some(config_file), scope, - parent, + workspace_root, settings, file_fetcher, ) @@ -1139,8 +1161,14 @@ impl ConfigData { specifier.as_str(), err ); - let mut data = - Self::load_inner(None, scope, parent, settings, file_fetcher).await; + let mut data = Self::load_inner( + None, + scope, + workspace_root, + settings, + file_fetcher, + ) + .await; data .watched_files .insert(specifier.clone(), ConfigWatchedFileType::DenoJson); @@ -1158,14 +1186,15 @@ impl ConfigData { } } } else { - Self::load_inner(None, scope, parent, settings, file_fetcher).await + Self::load_inner(None, scope, workspace_root, settings, file_fetcher) + .await } } async fn load_inner( config_file: Option, scope: &ModuleSpecifier, - parent: Option<(&ModuleSpecifier, &ConfigData)>, + workspace_root: Option<(&ModuleSpecifier, &ConfigData)>, settings: &Settings, file_fetcher: Option<&Arc>, ) -> Self { @@ -1190,12 +1219,12 @@ impl ConfigData { } let mut fmt_options = None; - if let Some((_, parent_data)) = parent { + if let Some((_, workspace_data)) = workspace_root { let has_own_fmt_options = config_file .as_ref() .is_some_and(|config_file| config_file.json.fmt.is_some()); if !has_own_fmt_options { - fmt_options = Some(parent_data.fmt_options.clone()) + fmt_options = Some(workspace_data.fmt_options.clone()) } } let fmt_options = fmt_options.unwrap_or_else(|| { @@ -1221,14 +1250,14 @@ impl ConfigData { }); let mut lint_options_rules = None; - if let Some((_, parent_data)) = parent { + if let Some((_, workspace_data)) = workspace_root { let has_own_lint_options = config_file .as_ref() .is_some_and(|config_file| config_file.json.lint.is_some()); if !has_own_lint_options { lint_options_rules = Some(( - parent_data.lint_options.clone(), - parent_data.lint_rules.clone(), + workspace_data.lint_options.clone(), + workspace_data.lint_rules.clone(), )) } } @@ -1474,6 +1503,44 @@ impl ConfigData { } } + let package_config = config_file.as_ref().and_then(|c| { + Some(LspPackageConfig { + nv: PackageNv { + name: c.json.name.clone()?, + version: Version::parse_standard(c.json.version.as_ref()?).ok()?, + }, + exports: c.json.exports.clone()?, + }) + }); + + let is_workspace_root = config_file + .as_ref() + .is_some_and(|c| !c.json.workspaces.is_empty()); + let workspace_members = if is_workspace_root { + Arc::new( + config_file + .as_ref() + .map(|c| { + c.json + .workspaces + .iter() + .flat_map(|p| { + let dir_specifier = c.specifier.join(p).ok()?; + let dir_path = specifier_to_file_path(&dir_specifier).ok()?; + Url::from_directory_path(normalize_path(dir_path)).ok() + }) + .collect() + }) + .unwrap_or_default(), + ) + } else if let Some((_, workspace_data)) = workspace_root { + workspace_data.workspace_members.clone() + } else if config_file.as_ref().is_some_and(|c| c.json.name.is_some()) { + Arc::new(vec![scope.clone()]) + } else { + Arc::new(vec![]) + }; + ConfigData { scope: scope.clone(), config_file: config_file.map(Arc::new), @@ -1490,6 +1557,9 @@ impl ConfigData { npmrc, import_map: import_map.map(Arc::new), import_map_from_settings, + package_config: package_config.map(Arc::new), + is_workspace_root, + workspace_members, watched_files, } } @@ -1639,27 +1709,57 @@ impl ConfigTree { } for specifier in workspace_files { - if specifier.path().ends_with("/deno.json") - || specifier.path().ends_with("/deno.jsonc") + if !(specifier.path().ends_with("/deno.json") + || specifier.path().ends_with("/deno.jsonc")) { - if let Ok(scope) = specifier.join(".") { - if !scopes.contains_key(&scope) { - let parent = scopes - .iter() - .rev() - .find(|(s, _)| scope.as_str().starts_with(s.as_str())); - let data = ConfigData::load( - Some(specifier), - &scope, - parent, - settings, - Some(file_fetcher), - ) - .await; - scopes.insert(scope, data); + continue; + } + let Ok(scope) = specifier.join(".") else { + continue; + }; + if scopes.contains_key(&scope) { + continue; + } + let data = ConfigData::load( + Some(specifier), + &scope, + None, + settings, + Some(file_fetcher), + ) + .await; + if data.is_workspace_root { + for member_scope in data.workspace_members.iter() { + if scopes.contains_key(member_scope) { + continue; } + let Ok(member_path) = specifier_to_file_path(member_scope) else { + continue; + }; + let Some(config_file_path) = Some(member_path.join("deno.json")) + .filter(|p| p.exists()) + .or_else(|| { + Some(member_path.join("deno.jsonc")).filter(|p| p.exists()) + }) + else { + continue; + }; + let Ok(config_file_specifier) = Url::from_file_path(config_file_path) + else { + continue; + }; + let member_data = ConfigData::load( + Some(&config_file_specifier), + member_scope, + Some((&scope, &data)), + settings, + Some(file_fetcher), + ) + .await; + scopes.insert(member_scope.clone(), member_data); } } + scopes.insert(scope, data); } for folder_uri in settings.by_workspace_folder.keys() { @@ -1741,8 +1841,11 @@ fn resolve_node_modules_dir( fn resolve_lockfile_from_path(lockfile_path: PathBuf) -> Option { match read_lockfile_at_path(lockfile_path) { Ok(value) => { - if let Ok(specifier) = ModuleSpecifier::from_file_path(&value.filename) { - lsp_log!(" Resolved lockfile: \"{}\"", specifier); + if value.filename.exists() { + if let Ok(specifier) = ModuleSpecifier::from_file_path(&value.filename) + { + lsp_log!(" Resolved lockfile: \"{}\"", specifier); + } } Some(value) } diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index c7323d0c85..5624ccb568 100644 --- a/cli/lsp/documents.rs +++ b/cli/lsp/documents.rs @@ -1056,7 +1056,7 @@ impl Documents { Cow::Owned( self .resolver - .jsr_to_registry_url(&jsr_req_ref, file_referrer)?, + .jsr_to_resource_url(&jsr_req_ref, file_referrer)?, ) } else { Cow::Borrowed(specifier) diff --git a/cli/lsp/jsr.rs b/cli/lsp/jsr.rs index 27db4b0c8d..52d48c1156 100644 --- a/cli/lsp/jsr.rs +++ b/cli/lsp/jsr.rs @@ -1,20 +1,287 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use crate::args::jsr_api_url; +use crate::args::jsr_url; use crate::file_fetcher::FileFetcher; +use crate::jsr::partial_jsr_package_version_info_from_slice; use crate::jsr::JsrFetchResolver; use dashmap::DashMap; +use deno_cache_dir::HttpCache; use deno_core::anyhow::anyhow; use deno_core::error::AnyError; use deno_core::serde_json; +use deno_graph::packages::JsrPackageInfo; +use deno_graph::packages::JsrPackageInfoVersion; +use deno_graph::packages::JsrPackageVersionInfo; +use deno_graph::ModuleSpecifier; use deno_runtime::deno_permissions::PermissionsContainer; +use deno_semver::jsr::JsrPackageReqReference; use deno_semver::package::PackageNv; +use deno_semver::package::PackageReq; use deno_semver::Version; use serde::Deserialize; +use std::borrow::Cow; +use std::collections::HashMap; use std::sync::Arc; +use super::config::Config; +use super::config::ConfigData; use super::search::PackageSearchApi; +/// Keep in sync with `JsrFetchResolver`! +#[derive(Debug)] +pub struct JsrCacheResolver { + nv_by_req: DashMap>, + /// The `module_graph` fields of the version infos should be forcibly absent. + /// It can be large and we don't want to store it. + info_by_nv: DashMap>>, + info_by_name: DashMap>>, + workspace_scope_by_name: HashMap, + cache: Arc, +} + +impl JsrCacheResolver { + pub fn new( + cache: Arc, + config_data: Option<&ConfigData>, + config: &Config, + ) -> Self { + let nv_by_req = DashMap::new(); + let info_by_nv = DashMap::new(); + let info_by_name = DashMap::new(); + let mut workspace_scope_by_name = HashMap::new(); + if let Some(config_data) = config_data { + let config_data_by_scope = config.tree.data_by_scope(); + for member_scope in config_data.workspace_members.as_ref() { + let Some(member_data) = config_data_by_scope.get(member_scope) else { + continue; + }; + let Some(package_config) = member_data.package_config.as_ref() else { + continue; + }; + info_by_name.insert( + package_config.nv.name.clone(), + Some(Arc::new(JsrPackageInfo { + versions: [( + package_config.nv.version.clone(), + JsrPackageInfoVersion { yanked: false }, + )] + .into_iter() + .collect(), + })), + ); + info_by_nv.insert( + package_config.nv.clone(), + Some(Arc::new(JsrPackageVersionInfo { + exports: package_config.exports.clone(), + module_graph_1: None, + module_graph_2: None, + manifest: Default::default(), + })), + ); + workspace_scope_by_name + .insert(package_config.nv.name.clone(), member_scope.clone()); + } + } + if let Some(lockfile) = config_data.and_then(|d| d.lockfile.as_ref()) { + for (req_url, nv_url) in &lockfile.lock().content.packages.specifiers { + let Some(req) = req_url.strip_prefix("jsr:") else { + continue; + }; + let Some(nv) = nv_url.strip_prefix("jsr:") else { + continue; + }; + let Ok(req) = PackageReq::from_str(req) else { + continue; + }; + let Ok(nv) = PackageNv::from_str(nv) else { + continue; + }; + nv_by_req.insert(req, Some(nv)); + } + } + Self { + nv_by_req, + info_by_nv, + info_by_name, + workspace_scope_by_name, + cache: cache.clone(), + } + } + + pub fn req_to_nv(&self, req: &PackageReq) -> Option { + if let Some(nv) = self.nv_by_req.get(req) { + return nv.value().clone(); + } + let maybe_get_nv = || { + let name = req.name.clone(); + let package_info = self.package_info(&name)?; + // Find the first matching version of the package which is cached. + let mut versions = package_info.versions.keys().collect::>(); + versions.sort(); + let version = versions + .into_iter() + .rev() + .find(|v| { + if req.version_req.tag().is_some() || !req.version_req.matches(v) { + return false; + } + let nv = PackageNv { + name: name.clone(), + version: (*v).clone(), + }; + self.package_version_info(&nv).is_some() + }) + .cloned()?; + Some(PackageNv { name, version }) + }; + let nv = maybe_get_nv(); + self.nv_by_req.insert(req.clone(), nv.clone()); + nv + } + + pub fn jsr_to_resource_url( + &self, + req_ref: &JsrPackageReqReference, + ) -> Option { + let req = req_ref.req().clone(); + let maybe_nv = self.req_to_nv(&req); + let nv = maybe_nv.as_ref()?; + let info = self.package_version_info(nv)?; + let path = info.export(&normalize_export_name(req_ref.sub_path()))?; + if let Some(workspace_scope) = self.workspace_scope_by_name.get(&nv.name) { + workspace_scope.join(path).ok() + } else { + jsr_url() + .join(&format!("{}/{}/{}", &nv.name, &nv.version, &path)) + .ok() + } + } + + pub fn lookup_export_for_path( + &self, + nv: &PackageNv, + path: &str, + ) -> Option { + let info = self.package_version_info(nv)?; + let path = path.strip_prefix("./").unwrap_or(path); + let mut sloppy_fallback = None; + for (export, path_) in info.exports() { + let path_ = path_.strip_prefix("./").unwrap_or(path_); + if path_ == path { + return Some(export.strip_prefix("./").unwrap_or(export).to_string()); + } + // TSC in some cases will suggest a `.js` import path for a `.d.ts` source + // file. + if sloppy_fallback.is_none() { + let path = path + .strip_suffix(".js") + .or_else(|| path.strip_suffix(".mjs")) + .or_else(|| path.strip_suffix(".cjs")) + .unwrap_or(path); + let path_ = path_ + .strip_suffix(".d.ts") + .or_else(|| path_.strip_suffix(".d.mts")) + .or_else(|| path_.strip_suffix(".d.cts")) + .unwrap_or(path_); + if path_ == path { + sloppy_fallback = + Some(export.strip_prefix("./").unwrap_or(export).to_string()); + } + } + } + sloppy_fallback + } + + pub fn lookup_req_for_nv(&self, nv: &PackageNv) -> Option { + for entry in self.nv_by_req.iter() { + let Some(nv_) = entry.value() else { + continue; + }; + if nv_ == nv { + return Some(entry.key().clone()); + } + } + None + } + + pub fn package_info(&self, name: &str) -> Option> { + if let Some(info) = self.info_by_name.get(name) { + return info.value().clone(); + } + let read_cached_package_info = || { + let meta_url = jsr_url().join(&format!("{}/meta.json", name)).ok()?; + let meta_bytes = read_cached_url(&meta_url, &self.cache)?; + serde_json::from_slice::(&meta_bytes).ok() + }; + let info = read_cached_package_info().map(Arc::new); + self.info_by_name.insert(name.to_string(), info.clone()); + info + } + + pub fn package_version_info( + &self, + nv: &PackageNv, + ) -> Option> { + if let Some(info) = self.info_by_nv.get(nv) { + return info.value().clone(); + } + let read_cached_package_version_info = || { + let meta_url = jsr_url() + .join(&format!("{}/{}_meta.json", &nv.name, &nv.version)) + .ok()?; + let meta_bytes = read_cached_url(&meta_url, &self.cache)?; + partial_jsr_package_version_info_from_slice(&meta_bytes).ok() + }; + let info = read_cached_package_version_info().map(Arc::new); + self.info_by_nv.insert(nv.clone(), info.clone()); + info + } + + pub fn did_cache(&self) { + self.nv_by_req.retain(|_, nv| nv.is_some()); + self.info_by_nv.retain(|_, info| info.is_some()); + self.info_by_name.retain(|_, info| info.is_some()); + } +} + +fn read_cached_url( + url: &ModuleSpecifier, + cache: &Arc, +) -> Option> { + cache + .read_file_bytes( + &cache.cache_item_key(url).ok()?, + None, + deno_cache_dir::GlobalToLocalCopy::Disallow, + ) + .ok()? +} + +// TODO(nayeemrmn): This is duplicated from a private function in deno_graph +// 0.65.1. Make it public or cleanup otherwise. +fn normalize_export_name(sub_path: Option<&str>) -> Cow { + let Some(sub_path) = sub_path else { + return Cow::Borrowed("."); + }; + if sub_path.is_empty() || matches!(sub_path, "/" | ".") { + Cow::Borrowed(".") + } else { + let sub_path = if sub_path.starts_with('/') { + Cow::Owned(format!(".{}", sub_path)) + } else if !sub_path.starts_with("./") { + Cow::Owned(format!("./{}", sub_path)) + } else { + Cow::Borrowed(sub_path) + }; + if let Some(prefix) = sub_path.strip_suffix('/') { + Cow::Owned(prefix.to_string()) + } else { + sub_path + } + } +} + #[derive(Debug)] pub struct CliJsrSearchApi { file_fetcher: Arc, diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 466c5b430c..f8a9652258 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -1471,7 +1471,7 @@ impl Inner { { if let Some(url) = self .resolver - .jsr_to_registry_url(&jsr_req_ref, file_referrer) + .jsr_to_resource_url(&jsr_req_ref, file_referrer) { result = format!("{result} (<{url}>)"); } diff --git a/cli/lsp/resolver.rs b/cli/lsp/resolver.rs index 348eae76f6..d0a515063b 100644 --- a/cli/lsp/resolver.rs +++ b/cli/lsp/resolver.rs @@ -5,7 +5,6 @@ use crate::args::package_json; use crate::args::CacheSetting; use crate::graph_util::CliJsrUrlProvider; use crate::http_util::HttpClientProvider; -use crate::jsr::JsrCacheResolver; use crate::lsp::config::Config; use crate::lsp::config::ConfigData; use crate::npm::create_cli_npm_resolver_for_lsp; @@ -51,6 +50,7 @@ use std::rc::Rc; use std::sync::Arc; use super::cache::LspCache; +use super::jsr::JsrCacheResolver; #[derive(Debug, Clone)] pub struct LspResolver { @@ -99,7 +99,8 @@ impl LspResolver { ); let jsr_resolver = Some(Arc::new(JsrCacheResolver::new( cache.root_vendor_or_global(), - config_data.and_then(|d| d.lockfile.clone()), + config_data, + config, ))); let redirect_resolver = Some(Arc::new(RedirectResolver::new( cache.root_vendor_or_global(), @@ -212,12 +213,12 @@ impl LspResolver { .collect() } - pub fn jsr_to_registry_url( + pub fn jsr_to_resource_url( &self, req_ref: &JsrPackageReqReference, _file_referrer: Option<&ModuleSpecifier>, ) -> Option { - self.jsr_resolver.as_ref()?.jsr_to_registry_url(req_ref) + self.jsr_resolver.as_ref()?.jsr_to_resource_url(req_ref) } pub fn jsr_lookup_export_for_path( diff --git a/tests/integration/lsp_tests.rs b/tests/integration/lsp_tests.rs index 581d436bb9..25fb695b48 100644 --- a/tests/integration/lsp_tests.rs +++ b/tests/integration/lsp_tests.rs @@ -11974,22 +11974,22 @@ fn lsp_vendor_dir() { client.shutdown(); } #[test] -fn lsp_deno_json_scopes_fmt_config() { +fn lsp_deno_json_workspace_fmt_config() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); - temp_dir.create_dir_all("project1"); temp_dir.write( - "project1/deno.json", + "deno.json", json!({ + "workspaces": ["project1", "project2"], "fmt": { "semiColons": false, }, }) .to_string(), ); - temp_dir.create_dir_all("project2"); + temp_dir.create_dir_all("project1"); temp_dir.write( - "project2/deno.json", + "project1/deno.json", json!({ "fmt": { "singleQuote": true, @@ -11997,13 +11997,13 @@ fn lsp_deno_json_scopes_fmt_config() { }) .to_string(), ); - temp_dir.create_dir_all("project2/project3"); - temp_dir.write("project2/project3/deno.json", json!({}).to_string()); + temp_dir.create_dir_all("project2"); + temp_dir.write("project2/deno.json", json!({}).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(), + "uri": temp_dir.uri().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": "console.log(\"\");\n", @@ -12013,7 +12013,7 @@ fn lsp_deno_json_scopes_fmt_config() { "textDocument/formatting", json!({ "textDocument": { - "uri": temp_dir.uri().join("project1/file.ts").unwrap(), + "uri": temp_dir.uri().join("file.ts").unwrap(), }, "options": { "tabSize": 2, @@ -12031,6 +12031,38 @@ fn lsp_deno_json_scopes_fmt_config() { "newText": "", }]) ); + client.did_open(json!({ + "textDocument": { + "uri": temp_dir.uri().join("project1/file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": "console.log(\"\");\n", + }, + })); + let res = client.write_request( + "textDocument/formatting", + json!({ + "textDocument": { + "uri": temp_dir.uri().join("project1/file.ts").unwrap(), + }, + "options": { + "tabSize": 2, + "insertSpaces": true, + }, + }), + ); + assert_eq!( + res, + json!([{ + "range": { + "start": { "line": 0, "character": 12 }, + "end": { "line": 0, "character": 14 }, + }, + "newText": "''", + }]) + ); + // `project2/file.ts` should use the fmt settings from `deno.json`, since it + // has no fmt field. client.did_open(json!({ "textDocument": { "uri": temp_dir.uri().join("project2/file.ts").unwrap(), @@ -12055,55 +12087,23 @@ fn lsp_deno_json_scopes_fmt_config() { res, json!([{ "range": { - "start": { "line": 0, "character": 12 }, - "end": { "line": 0, "character": 14 }, + "start": { "line": 0, "character": 15 }, + "end": { "line": 0, "character": 16 }, }, - "newText": "''", - }]) - ); - // `project2/project3/file.ts` should use the fmt settings from - // `project2/deno.json`, since `project2/project3/deno.json` has no fmt field. - client.did_open(json!({ - "textDocument": { - "uri": temp_dir.uri().join("project2/project3/file.ts").unwrap(), - "languageId": "typescript", - "version": 1, - "text": "console.log(\"\");\n", - }, - })); - let res = client.write_request( - "textDocument/formatting", - json!({ - "textDocument": { - "uri": temp_dir.uri().join("project2/project3/file.ts").unwrap(), - }, - "options": { - "tabSize": 2, - "insertSpaces": true, - }, - }), - ); - assert_eq!( - res, - json!([{ - "range": { - "start": { "line": 0, "character": 12 }, - "end": { "line": 0, "character": 14 }, - }, - "newText": "''", + "newText": "", }]) ); client.shutdown(); } #[test] -fn lsp_deno_json_scopes_lint_config() { +fn lsp_deno_json_workspace_lint_config() { let context = TestContextBuilder::new().use_temp_cwd().build(); let temp_dir = context.temp_dir(); - temp_dir.create_dir_all("project1"); temp_dir.write( - "project1/deno.json", + "deno.json", json!({ + "workspaces": ["project1", "project2"], "lint": { "rules": { "include": ["camelcase"], @@ -12112,9 +12112,9 @@ fn lsp_deno_json_scopes_lint_config() { }) .to_string(), ); - temp_dir.create_dir_all("project2"); + temp_dir.create_dir_all("project1"); temp_dir.write( - "project2/deno.json", + "project1/deno.json", json!({ "lint": { "rules": { @@ -12124,13 +12124,13 @@ fn lsp_deno_json_scopes_lint_config() { }) .to_string(), ); - temp_dir.create_dir_all("project2/project3"); - temp_dir.write("project2/project3/deno.json", json!({}).to_string()); + temp_dir.create_dir_all("project2"); + temp_dir.write("project2/deno.json", json!({}).to_string()); let mut client = context.new_lsp_command().build(); client.initialize_default(); let diagnostics = client.did_open(json!({ "textDocument": { - "uri": temp_dir.uri().join("project1/file.ts").unwrap(), + "uri": temp_dir.uri().join("file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#" @@ -12143,7 +12143,7 @@ fn lsp_deno_json_scopes_lint_config() { assert_eq!( json!(diagnostics.messages_with_source("deno-lint")), json!({ - "uri": temp_dir.uri().join("project1/file.ts").unwrap(), + "uri": temp_dir.uri().join("file.ts").unwrap(), "diagnostics": [{ "range": { "start": { "line": 2, "character": 14 }, @@ -12161,13 +12161,13 @@ fn lsp_deno_json_scopes_lint_config() { "textDocument/didClose", json!({ "textDocument": { - "uri": temp_dir.uri().join("project1/file.ts").unwrap(), + "uri": temp_dir.uri().join("file.ts").unwrap(), }, }), ); let diagnostics = client.did_open(json!({ "textDocument": { - "uri": temp_dir.uri().join("project2/file.ts").unwrap(), + "uri": temp_dir.uri().join("project1/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#" @@ -12180,7 +12180,7 @@ fn lsp_deno_json_scopes_lint_config() { assert_eq!( json!(diagnostics.messages_with_source("deno-lint")), json!({ - "uri": temp_dir.uri().join("project2/file.ts").unwrap(), + "uri": temp_dir.uri().join("project1/file.ts").unwrap(), "diagnostics": [{ "range": { "start": { "line": 1, "character": 8 }, @@ -12198,16 +12198,15 @@ fn lsp_deno_json_scopes_lint_config() { "textDocument/didClose", json!({ "textDocument": { - "uri": temp_dir.uri().join("project2/file.ts").unwrap(), + "uri": temp_dir.uri().join("project1/file.ts").unwrap(), }, }), ); - // `project2/project3/file.ts` should use the lint settings from - // `project2/deno.json`, since `project2/project3/deno.json` has no lint - // field. + // `project2/file.ts` should use the lint settings from `deno.json`, since it + // has no lint field. let diagnostics = client.did_open(json!({ "textDocument": { - "uri": temp_dir.uri().join("project2/project3/file.ts").unwrap(), + "uri": temp_dir.uri().join("project2/file.ts").unwrap(), "languageId": "typescript", "version": 1, "text": r#" @@ -12220,16 +12219,16 @@ fn lsp_deno_json_scopes_lint_config() { assert_eq!( json!(diagnostics.messages_with_source("deno-lint")), json!({ - "uri": temp_dir.uri().join("project2/project3/file.ts").unwrap(), + "uri": temp_dir.uri().join("project2/file.ts").unwrap(), "diagnostics": [{ "range": { - "start": { "line": 1, "character": 8 }, - "end": { "line": 1, "character": 27 }, + "start": { "line": 2, "character": 14 }, + "end": { "line": 2, "character": 28 }, }, "severity": 2, - "code": "ban-untagged-todo", + "code": "camelcase", "source": "deno-lint", - "message": "TODO should be tagged with (@username) or (#issue)\nAdd a user tag or issue reference to the TODO comment, e.g. TODO(@djones), TODO(djones), TODO(#123)", + "message": "Identifier 'snake_case_var' is not in camel case.\nConsider renaming `snake_case_var` to `snakeCaseVar`", }], "version": 1, }) @@ -12237,6 +12236,64 @@ fn lsp_deno_json_scopes_lint_config() { client.shutdown(); } +#[test] +fn lsp_deno_json_workspace_jsr_resolution() { + let context = TestContextBuilder::new().use_temp_cwd().build(); + let temp_dir = context.temp_dir(); + temp_dir.write( + "deno.json", + json!({ + "workspaces": ["project1"], + }) + .to_string(), + ); + temp_dir.create_dir_all("project1"); + temp_dir.write( + "project1/deno.json", + json!({ + "name": "@org/project1", + "version": "1.0.0", + "exports": { + ".": "./mod.ts", + }, + }) + .to_string(), + ); + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + client.did_open(json!({ + "textDocument": { + "uri": temp_dir.uri().join("file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": "import \"jsr:@org/project1@^1.0.0\";\n", + }, + })); + let res = client.write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": temp_dir.uri().join("file.ts").unwrap(), + }, + "position": { "line": 0, "character": 7 }, + }), + ); + assert_eq!( + res, + json!({ + "contents": { + "kind": "markdown", + "value": format!("**Resolved Dependency**\n\n**Code**: jsr​:​@org/project1​@^1.0.0 (<{}project1/mod.ts>)\n", temp_dir.uri()), + }, + "range": { + "start": { "line": 0, "character": 7 }, + "end": { "line": 0, "character": 33 }, + }, + }), + ); + client.shutdown(); +} + #[test] fn lsp_import_unstable_bare_node_builtins_auto_discovered() { let context = TestContextBuilder::new().use_temp_cwd().build();