diff --git a/cli/args/mod.rs b/cli/args/mod.rs index 7fcc56c5f7..95df047e63 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -693,6 +693,7 @@ impl CliOptions { maybe_config_file: Option, maybe_lockfile: Option>>, maybe_package_json: Option, + force_global_cache: bool, ) -> Result { if let Some(insecure_allowlist) = flags.unsafely_ignore_certificate_errors.as_ref() @@ -708,6 +709,7 @@ impl CliOptions { eprintln!("{}", colors::yellow(msg)); } + let maybe_lockfile = maybe_lockfile.filter(|_| !force_global_cache); let maybe_node_modules_folder = resolve_node_modules_folder( &initial_cwd, &flags, @@ -715,8 +717,11 @@ impl CliOptions { maybe_package_json.as_ref(), ) .with_context(|| "Resolving node_modules folder.")?; - let maybe_vendor_folder = - resolve_vendor_folder(&initial_cwd, &flags, maybe_config_file.as_ref()); + let maybe_vendor_folder = if force_global_cache { + None + } else { + resolve_vendor_folder(&initial_cwd, &flags, maybe_config_file.as_ref()) + }; let maybe_workspace_config = if let Some(config_file) = maybe_config_file.as_ref() { config_file.to_workspace_config()? @@ -802,6 +807,7 @@ impl CliOptions { maybe_config_file, maybe_lock_file.map(|l| Arc::new(Mutex::new(l))), maybe_package_json, + false, ) } diff --git a/cli/file_fetcher.rs b/cli/file_fetcher.rs index a74a14a3fa..5b530cf6dd 100644 --- a/cli/file_fetcher.rs +++ b/cli/file_fetcher.rs @@ -186,6 +186,10 @@ impl FileFetcher { } } + pub fn http_cache(&self) -> &Arc { + &self.http_cache + } + pub fn cache_setting(&self) -> &CacheSetting { &self.cache_setting } diff --git a/cli/lsp/completions.rs b/cli/lsp/completions.rs index 7fe1610390..074d913c5e 100644 --- a/cli/lsp/completions.rs +++ b/cli/lsp/completions.rs @@ -5,10 +5,12 @@ use super::config::ConfigSnapshot; use super::config::WorkspaceSettings; use super::documents::Documents; use super::documents::DocumentsFilter; +use super::jsr::CliJsrSearchApi; +use super::jsr::JsrResolver; use super::lsp_custom; use super::npm::CliNpmSearchApi; -use super::npm::NpmSearchApi; use super::registries::ModuleRegistry; +use super::search::PackageSearchApi; use super::tsc; use crate::util::path::is_importable_ext; @@ -25,6 +27,8 @@ use deno_core::serde::Serialize; use deno_core::serde_json::json; use deno_core::url::Position; use deno_core::ModuleSpecifier; +use deno_semver::jsr::JsrPackageReqReference; +use deno_semver::package::PackageNv; use import_map::ImportMap; use once_cell::sync::Lazy; use regex::Regex; @@ -148,6 +152,7 @@ pub async fn get_import_completions( config: &ConfigSnapshot, client: &Client, module_registries: &ModuleRegistry, + jsr_search_api: &CliJsrSearchApi, npm_search_api: &CliNpmSearchApi, documents: &Documents, maybe_import_map: Option>, @@ -170,6 +175,19 @@ pub async fn get_import_completions( is_incomplete: false, items: get_local_completions(specifier, &text, &range)?, })) + } else if text.starts_with("jsr:") { + let items = get_jsr_completions( + specifier, + &text, + &range, + jsr_search_api, + jsr_search_api.get_resolver(), + ) + .await?; + Some(lsp::CompletionResponse::List(lsp::CompletionList { + is_incomplete: !items.is_empty(), + items, + })) } else if text.starts_with("npm:") { let items = get_npm_completions(specifier, &text, &range, npm_search_api).await?; @@ -475,8 +493,7 @@ fn get_relative_specifiers( } /// Find the index of the '@' delimiting the package name and version, if any. -fn parse_npm_specifier_version_index(specifier: &str) -> Option { - let bare_specifier = specifier.strip_prefix("npm:")?; +fn parse_bare_specifier_version_index(bare_specifier: &str) -> Option { if bare_specifier.starts_with('@') { bare_specifier .find('/') @@ -486,49 +503,188 @@ fn parse_npm_specifier_version_index(specifier: &str) -> Option { .find('@') .filter(|idx2| !bare_specifier[idx..][1..*idx2].is_empty()) .filter(|idx2| !bare_specifier[idx..][1..*idx2].contains('/')) - .map(|idx2| 4 + idx + idx2) + .map(|idx2| idx + idx2) }) } else { bare_specifier .find('@') .filter(|idx| !bare_specifier[1..*idx].is_empty()) .filter(|idx| !bare_specifier[1..*idx].contains('/')) - .map(|idx| 4 + idx) } } +async fn get_jsr_completions( + referrer: &ModuleSpecifier, + specifier: &str, + range: &lsp::Range, + jsr_search_api: &impl PackageSearchApi, + jsr_resolver: &JsrResolver, +) -> Option> { + // First try to match `jsr:some-package@some-version/`. + if let Ok(req_ref) = JsrPackageReqReference::from_str(specifier) { + let sub_path = req_ref.sub_path(); + if sub_path.is_some() || specifier.ends_with('/') { + let export_prefix = sub_path.unwrap_or(""); + let req = req_ref.req(); + let nv = jsr_resolver.req_to_nv(req); + let nv = nv.or_else(|| PackageNv::from_str(&req.to_string()).ok())?; + let exports = jsr_search_api.exports(&nv).await.ok()?; + let items = exports + .iter() + .enumerate() + .filter_map(|(idx, export)| { + if export == "." { + return None; + } + let export = export.strip_prefix("./").unwrap_or(export.as_str()); + if !export.starts_with(export_prefix) { + return None; + } + let specifier = format!("jsr:{}/{}", req_ref.req(), export); + let command = Some(lsp::Command { + title: "".to_string(), + command: "deno.cache".to_string(), + arguments: Some(vec![ + json!([&specifier]), + json!(referrer), + json!({ "forceGlobalCache": true }), + ]), + }); + let text_edit = Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: *range, + new_text: specifier.clone(), + })); + Some(lsp::CompletionItem { + label: specifier, + kind: Some(lsp::CompletionItemKind::FILE), + detail: Some("(jsr)".to_string()), + sort_text: Some(format!("{:0>10}", idx + 1)), + text_edit, + command, + commit_characters: Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(), + ), + ..Default::default() + }) + }) + .collect(); + return Some(items); + } + } + + // Then try to match `jsr:some-package@`. + let bare_specifier = specifier.strip_prefix("jsr:")?; + if let Some(v_index) = parse_bare_specifier_version_index(bare_specifier) { + let package_name = &bare_specifier[..v_index]; + let v_prefix = &bare_specifier[(v_index + 1)..]; + + let versions = jsr_search_api.versions(package_name).await.ok()?; + let items = versions + .iter() + .enumerate() + .filter_map(|(idx, version)| { + let version = version.to_string(); + if !version.starts_with(v_prefix) { + return None; + } + let specifier = format!("jsr:{}@{}", package_name, version); + let command = Some(lsp::Command { + title: "".to_string(), + command: "deno.cache".to_string(), + arguments: Some(vec![ + json!([&specifier]), + json!(referrer), + json!({ "forceGlobalCache": true }), + ]), + }); + let text_edit = Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: *range, + new_text: specifier.clone(), + })); + Some(lsp::CompletionItem { + label: specifier, + kind: Some(lsp::CompletionItemKind::FILE), + detail: Some("(jsr)".to_string()), + sort_text: Some(format!("{:0>10}", idx + 1)), + text_edit, + command, + commit_characters: Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(), + ), + ..Default::default() + }) + }) + .collect(); + return Some(items); + } + + // Otherwise match `jsr:`. + let names = jsr_search_api.search(bare_specifier).await.ok()?; + let items = names + .iter() + .enumerate() + .map(|(idx, name)| { + let specifier = format!("jsr:{}", name); + let command = Some(lsp::Command { + title: "".to_string(), + command: "deno.cache".to_string(), + arguments: Some(vec![ + json!([&specifier]), + json!(referrer), + json!({ "forceGlobalCache": true }), + ]), + }); + let text_edit = Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: *range, + new_text: specifier.clone(), + })); + lsp::CompletionItem { + label: specifier, + kind: Some(lsp::CompletionItemKind::FILE), + detail: Some("(jsr)".to_string()), + sort_text: Some(format!("{:0>10}", idx + 1)), + text_edit, + command, + commit_characters: Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(), + ), + ..Default::default() + } + }) + .collect(); + Some(items) +} + /// Get completions for `npm:` specifiers. async fn get_npm_completions( referrer: &ModuleSpecifier, specifier: &str, range: &lsp::Range, - npm_search_api: &impl NpmSearchApi, + npm_search_api: &impl PackageSearchApi, ) -> Option> { // First try to match `npm:some-package@`. - if let Some(v_index) = parse_npm_specifier_version_index(specifier) { - let package_name = &specifier[..v_index].strip_prefix("npm:")?; - let v_prefix = &specifier[(v_index + 1)..]; - let versions = &npm_search_api - .package_info(package_name) - .await - .ok()? - .versions; - let mut versions = versions.keys().collect::>(); - versions.sort(); + let bare_specifier = specifier.strip_prefix("npm:")?; + if let Some(v_index) = parse_bare_specifier_version_index(bare_specifier) { + let package_name = &bare_specifier[..v_index]; + let v_prefix = &bare_specifier[(v_index + 1)..]; + let versions = npm_search_api.versions(package_name).await.ok()?; let items = versions - .into_iter() - .rev() + .iter() .enumerate() .filter_map(|(idx, version)| { let version = version.to_string(); if !version.starts_with(v_prefix) { return None; } - let specifier = format!("npm:{}@{}", package_name, &version); + let specifier = format!("npm:{}@{}", package_name, version); let command = Some(lsp::Command { title: "".to_string(), command: "deno.cache".to_string(), - arguments: Some(vec![json!([&specifier]), json!(referrer)]), + arguments: Some(vec![ + json!([&specifier]), + json!(referrer), + json!({ "forceGlobalCache": true }), + ]), }); let text_edit = Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { range: *range, @@ -552,8 +708,7 @@ async fn get_npm_completions( } // Otherwise match `npm:`. - let package_name_prefix = specifier.strip_prefix("npm:")?; - let names = npm_search_api.search(package_name_prefix).await.ok()?; + let names = npm_search_api.search(bare_specifier).await.ok()?; let items = names .iter() .enumerate() @@ -562,7 +717,11 @@ async fn get_npm_completions( let command = Some(lsp::Command { title: "".to_string(), command: "deno.cache".to_string(), - arguments: Some(vec![json!([&specifier]), json!(referrer)]), + arguments: Some(vec![ + json!([&specifier]), + json!(referrer), + json!({ "forceGlobalCache": true }), + ]), }); let text_edit = Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { range: *range, @@ -640,43 +799,16 @@ mod tests { use super::*; use crate::cache::GlobalHttpCache; use crate::cache::HttpCache; + use crate::cache::RealDenoCacheEnv; use crate::lsp::documents::Documents; use crate::lsp::documents::LanguageId; - use crate::lsp::npm::NpmSearchApi; - use crate::AnyError; - use async_trait::async_trait; + use crate::lsp::search::tests::TestPackageSearchApi; use deno_core::resolve_url; use deno_graph::Range; - use deno_npm::registry::NpmPackageInfo; - use deno_npm::registry::NpmRegistryApi; - use deno_npm::registry::TestNpmRegistryApi; use std::collections::HashMap; use std::path::Path; use test_util::TempDir; - #[derive(Default)] - struct TestNpmSearchApi( - HashMap>>, - TestNpmRegistryApi, - ); - - #[async_trait] - impl NpmSearchApi for TestNpmSearchApi { - async fn search(&self, query: &str) -> Result>, AnyError> { - match self.0.get(query) { - Some(names) => Ok(names.clone()), - None => Ok(Arc::new(vec![])), - } - } - - async fn package_info( - &self, - name: &str, - ) -> Result, AnyError> { - self.1.package_info(name).await.map_err(|e| e.into()) - } - } - fn mock_documents( fixtures: &[(&str, &str, i32, LanguageId)], source_fixtures: &[(&str, &str)], @@ -846,52 +978,326 @@ mod tests { } #[test] - fn test_parse_npm_specifier_version_index() { - assert_eq!(parse_npm_specifier_version_index("npm:"), None); - assert_eq!(parse_npm_specifier_version_index("npm:/"), None); - assert_eq!(parse_npm_specifier_version_index("npm:/@"), None); - assert_eq!(parse_npm_specifier_version_index("npm:@"), None); - assert_eq!(parse_npm_specifier_version_index("npm:@/"), None); - assert_eq!(parse_npm_specifier_version_index("npm:@/@"), None); - assert_eq!(parse_npm_specifier_version_index("npm:foo"), None); - assert_eq!(parse_npm_specifier_version_index("npm:foo/bar"), None); - assert_eq!(parse_npm_specifier_version_index("npm:foo/bar@"), None); - assert_eq!(parse_npm_specifier_version_index("npm:@org/foo/bar"), None); - assert_eq!(parse_npm_specifier_version_index("npm:@org/foo/bar@"), None); + fn test_parse_bare_specifier_version_index() { + assert_eq!(parse_bare_specifier_version_index(""), None); + assert_eq!(parse_bare_specifier_version_index("/"), None); + assert_eq!(parse_bare_specifier_version_index("/@"), None); + assert_eq!(parse_bare_specifier_version_index("@"), None); + assert_eq!(parse_bare_specifier_version_index("@/"), None); + assert_eq!(parse_bare_specifier_version_index("@/@"), None); + assert_eq!(parse_bare_specifier_version_index("foo"), None); + assert_eq!(parse_bare_specifier_version_index("foo/bar"), None); + assert_eq!(parse_bare_specifier_version_index("foo/bar@"), None); + assert_eq!(parse_bare_specifier_version_index("@org/foo/bar"), None); + assert_eq!(parse_bare_specifier_version_index("@org/foo/bar@"), None); - assert_eq!(parse_npm_specifier_version_index("npm:foo@"), Some(7)); - assert_eq!(parse_npm_specifier_version_index("npm:foo@1."), Some(7)); - assert_eq!(parse_npm_specifier_version_index("npm:@org/foo@"), Some(12)); - assert_eq!( - parse_npm_specifier_version_index("npm:@org/foo@1."), - Some(12) - ); + assert_eq!(parse_bare_specifier_version_index("foo@"), Some(3)); + assert_eq!(parse_bare_specifier_version_index("foo@1."), Some(3)); + assert_eq!(parse_bare_specifier_version_index("@org/foo@"), Some(8)); + assert_eq!(parse_bare_specifier_version_index("@org/foo@1."), Some(8)); // Regression test for https://github.com/denoland/deno/issues/22325. assert_eq!( - parse_npm_specifier_version_index( - "npm:@longer_than_right_one/arbitrary_string@" + parse_bare_specifier_version_index( + "@longer_than_right_one/arbitrary_string@" ), - Some(43) + Some(39) + ); + } + + #[tokio::test] + async fn test_get_jsr_completions() { + let temp_dir = TempDir::default(); + let jsr_resolver = JsrResolver::from_cache_and_lockfile( + Arc::new(GlobalHttpCache::new( + temp_dir.path().to_path_buf(), + RealDenoCacheEnv, + )), + None, + ); + let jsr_search_api = TestPackageSearchApi::default() + .with_package_version("@std/archive", "1.0.0", &[]) + .with_package_version("@std/assert", "1.0.0", &[]) + .with_package_version("@std/async", "1.0.0", &[]) + .with_package_version("@std/bytes", "1.0.0", &[]); + let range = lsp::Range { + start: lsp::Position { + line: 0, + character: 23, + }, + end: lsp::Position { + line: 0, + character: 29, + }, + }; + let referrer = ModuleSpecifier::parse("file:///referrer.ts").unwrap(); + let actual = get_jsr_completions( + &referrer, + "jsr:as", + &range, + &jsr_search_api, + &jsr_resolver, + ) + .await + .unwrap(); + assert_eq!( + actual, + vec![ + lsp::CompletionItem { + label: "jsr:@std/assert".to_string(), + kind: Some(lsp::CompletionItemKind::FILE), + detail: Some("(jsr)".to_string()), + sort_text: Some("0000000001".to_string()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range, + new_text: "jsr:@std/assert".to_string(), + })), + command: Some(lsp::Command { + title: "".to_string(), + command: "deno.cache".to_string(), + arguments: Some(vec![ + json!(["jsr:@std/assert"]), + json!(&referrer), + json!({ "forceGlobalCache": true }) + ]) + }), + commit_characters: Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect() + ), + ..Default::default() + }, + lsp::CompletionItem { + label: "jsr:@std/async".to_string(), + kind: Some(lsp::CompletionItemKind::FILE), + detail: Some("(jsr)".to_string()), + sort_text: Some("0000000002".to_string()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range, + new_text: "jsr:@std/async".to_string(), + })), + command: Some(lsp::Command { + title: "".to_string(), + command: "deno.cache".to_string(), + arguments: Some(vec![ + json!(["jsr:@std/async"]), + json!(&referrer), + json!({ "forceGlobalCache": true }) + ]) + }), + commit_characters: Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect() + ), + ..Default::default() + }, + ] + ); + } + + #[tokio::test] + async fn test_get_jsr_completions_for_versions() { + let temp_dir = TempDir::default(); + let jsr_resolver = JsrResolver::from_cache_and_lockfile( + Arc::new(GlobalHttpCache::new( + temp_dir.path().to_path_buf(), + RealDenoCacheEnv, + )), + None, + ); + let jsr_search_api = TestPackageSearchApi::default() + .with_package_version("@std/assert", "0.3.0", &[]) + .with_package_version("@std/assert", "0.4.0", &[]) + .with_package_version("@std/assert", "0.5.0", &[]); + let range = lsp::Range { + start: lsp::Position { + line: 0, + character: 23, + }, + end: lsp::Position { + line: 0, + character: 39, + }, + }; + let referrer = ModuleSpecifier::parse("file:///referrer.ts").unwrap(); + let actual = get_jsr_completions( + &referrer, + "jsr:@std/assert@", + &range, + &jsr_search_api, + &jsr_resolver, + ) + .await + .unwrap(); + assert_eq!( + actual, + vec![ + lsp::CompletionItem { + label: "jsr:@std/assert@0.5.0".to_string(), + kind: Some(lsp::CompletionItemKind::FILE), + detail: Some("(jsr)".to_string()), + sort_text: Some("0000000001".to_string()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range, + new_text: "jsr:@std/assert@0.5.0".to_string(), + })), + command: Some(lsp::Command { + title: "".to_string(), + command: "deno.cache".to_string(), + arguments: Some(vec![ + json!(["jsr:@std/assert@0.5.0"]), + json!(&referrer), + json!({ "forceGlobalCache": true }), + ]) + }), + commit_characters: Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect() + ), + ..Default::default() + }, + lsp::CompletionItem { + label: "jsr:@std/assert@0.4.0".to_string(), + kind: Some(lsp::CompletionItemKind::FILE), + detail: Some("(jsr)".to_string()), + sort_text: Some("0000000002".to_string()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range, + new_text: "jsr:@std/assert@0.4.0".to_string(), + })), + command: Some(lsp::Command { + title: "".to_string(), + command: "deno.cache".to_string(), + arguments: Some(vec![ + json!(["jsr:@std/assert@0.4.0"]), + json!(&referrer), + json!({ "forceGlobalCache": true }), + ]) + }), + commit_characters: Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect() + ), + ..Default::default() + }, + lsp::CompletionItem { + label: "jsr:@std/assert@0.3.0".to_string(), + kind: Some(lsp::CompletionItemKind::FILE), + detail: Some("(jsr)".to_string()), + sort_text: Some("0000000003".to_string()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range, + new_text: "jsr:@std/assert@0.3.0".to_string(), + })), + command: Some(lsp::Command { + title: "".to_string(), + command: "deno.cache".to_string(), + arguments: Some(vec![ + json!(["jsr:@std/assert@0.3.0"]), + json!(&referrer), + json!({ "forceGlobalCache": true }), + ]) + }), + commit_characters: Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect() + ), + ..Default::default() + }, + ] + ); + } + + #[tokio::test] + async fn test_get_jsr_completions_for_exports() { + let temp_dir = TempDir::default(); + let jsr_resolver = JsrResolver::from_cache_and_lockfile( + Arc::new(GlobalHttpCache::new( + temp_dir.path().to_path_buf(), + RealDenoCacheEnv, + )), + None, + ); + let jsr_search_api = TestPackageSearchApi::default().with_package_version( + "@std/path", + "0.1.0", + &[".", "./basename", "./common", "./constants", "./dirname"], + ); + let range = lsp::Range { + start: lsp::Position { + line: 0, + character: 23, + }, + end: lsp::Position { + line: 0, + character: 45, + }, + }; + let referrer = ModuleSpecifier::parse("file:///referrer.ts").unwrap(); + let actual = get_jsr_completions( + &referrer, + "jsr:@std/path@0.1.0/co", + &range, + &jsr_search_api, + &jsr_resolver, + ) + .await + .unwrap(); + assert_eq!( + actual, + vec![ + lsp::CompletionItem { + label: "jsr:@std/path@0.1.0/common".to_string(), + kind: Some(lsp::CompletionItemKind::FILE), + detail: Some("(jsr)".to_string()), + sort_text: Some("0000000003".to_string()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range, + new_text: "jsr:@std/path@0.1.0/common".to_string(), + })), + command: Some(lsp::Command { + title: "".to_string(), + command: "deno.cache".to_string(), + arguments: Some(vec![ + json!(["jsr:@std/path@0.1.0/common"]), + json!(&referrer), + json!({ "forceGlobalCache": true }), + ]) + }), + commit_characters: Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect() + ), + ..Default::default() + }, + lsp::CompletionItem { + label: "jsr:@std/path@0.1.0/constants".to_string(), + kind: Some(lsp::CompletionItemKind::FILE), + detail: Some("(jsr)".to_string()), + sort_text: Some("0000000004".to_string()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range, + new_text: "jsr:@std/path@0.1.0/constants".to_string(), + })), + command: Some(lsp::Command { + title: "".to_string(), + command: "deno.cache".to_string(), + arguments: Some(vec![ + json!(["jsr:@std/path@0.1.0/constants"]), + json!(&referrer), + json!({ "forceGlobalCache": true }), + ]) + }), + commit_characters: Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect() + ), + ..Default::default() + }, + ] ); } #[tokio::test] async fn test_get_npm_completions() { - let npm_search_api = TestNpmSearchApi( - vec![( - "puppe".to_string(), - Arc::new(vec![ - "puppeteer".to_string(), - "puppeteer-core".to_string(), - "puppeteer-extra-plugin-stealth".to_string(), - "puppeteer-extra-plugin".to_string(), - ]), - )] - .into_iter() - .collect(), - Default::default(), - ); + let npm_search_api = TestPackageSearchApi::default() + .with_package_version("puppeteer", "1.0.0", &[]) + .with_package_version("puppeteer-core", "1.0.0", &[]) + .with_package_version("puppeteer-extra-plugin", "1.0.0", &[]) + .with_package_version("puppeteer-extra-plugin-stealth", "1.0.0", &[]); let range = lsp::Range { start: lsp::Position { line: 0, @@ -922,7 +1328,11 @@ mod tests { command: Some(lsp::Command { title: "".to_string(), command: "deno.cache".to_string(), - arguments: Some(vec![json!(["npm:puppeteer"]), json!(&referrer)]) + arguments: Some(vec![ + json!(["npm:puppeteer"]), + json!(&referrer), + json!({ "forceGlobalCache": true }), + ]) }), commit_characters: Some( IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect() @@ -943,29 +1353,8 @@ mod tests { command: "deno.cache".to_string(), arguments: Some(vec![ json!(["npm:puppeteer-core"]), - json!(&referrer) - ]) - }), - commit_characters: Some( - IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect() - ), - ..Default::default() - }, - lsp::CompletionItem { - label: "npm:puppeteer-extra-plugin-stealth".to_string(), - kind: Some(lsp::CompletionItemKind::FILE), - detail: Some("(npm)".to_string()), - sort_text: Some("0000000003".to_string()), - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range, - new_text: "npm:puppeteer-extra-plugin-stealth".to_string(), - })), - command: Some(lsp::Command { - title: "".to_string(), - command: "deno.cache".to_string(), - arguments: Some(vec![ - json!(["npm:puppeteer-extra-plugin-stealth"]), - json!(&referrer) + json!(&referrer), + json!({ "forceGlobalCache": true }), ]) }), commit_characters: Some( @@ -977,7 +1366,7 @@ mod tests { label: "npm:puppeteer-extra-plugin".to_string(), kind: Some(lsp::CompletionItemKind::FILE), detail: Some("(npm)".to_string()), - sort_text: Some("0000000004".to_string()), + sort_text: Some("0000000003".to_string()), text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { range, new_text: "npm:puppeteer-extra-plugin".to_string(), @@ -987,7 +1376,31 @@ mod tests { command: "deno.cache".to_string(), arguments: Some(vec![ json!(["npm:puppeteer-extra-plugin"]), - json!(&referrer) + json!(&referrer), + json!({ "forceGlobalCache": true }), + ]) + }), + commit_characters: Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect() + ), + ..Default::default() + }, + lsp::CompletionItem { + label: "npm:puppeteer-extra-plugin-stealth".to_string(), + kind: Some(lsp::CompletionItemKind::FILE), + detail: Some("(npm)".to_string()), + sort_text: Some("0000000004".to_string()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range, + new_text: "npm:puppeteer-extra-plugin-stealth".to_string(), + })), + command: Some(lsp::Command { + title: "".to_string(), + command: "deno.cache".to_string(), + arguments: Some(vec![ + json!(["npm:puppeteer-extra-plugin-stealth"]), + json!(&referrer), + json!({ "forceGlobalCache": true }), ]) }), commit_characters: Some( @@ -1001,19 +1414,11 @@ mod tests { #[tokio::test] async fn test_get_npm_completions_for_versions() { - let npm_search_api = TestNpmSearchApi::default(); - npm_search_api - .1 - .ensure_package_version("puppeteer", "20.9.0"); - npm_search_api - .1 - .ensure_package_version("puppeteer", "21.0.0"); - npm_search_api - .1 - .ensure_package_version("puppeteer", "21.0.1"); - npm_search_api - .1 - .ensure_package_version("puppeteer", "21.0.2"); + let npm_search_api = TestPackageSearchApi::default() + .with_package_version("puppeteer", "20.9.0", &[]) + .with_package_version("puppeteer", "21.0.0", &[]) + .with_package_version("puppeteer", "21.0.1", &[]) + .with_package_version("puppeteer", "21.0.2", &[]); let range = lsp::Range { start: lsp::Position { line: 0, @@ -1046,7 +1451,8 @@ mod tests { command: "deno.cache".to_string(), arguments: Some(vec![ json!(["npm:puppeteer@21.0.2"]), - json!(&referrer) + json!(&referrer), + json!({ "forceGlobalCache": true }), ]) }), commit_characters: Some( @@ -1068,7 +1474,8 @@ mod tests { command: "deno.cache".to_string(), arguments: Some(vec![ json!(["npm:puppeteer@21.0.1"]), - json!(&referrer) + json!(&referrer), + json!({ "forceGlobalCache": true }), ]) }), commit_characters: Some( @@ -1090,7 +1497,8 @@ mod tests { command: "deno.cache".to_string(), arguments: Some(vec![ json!(["npm:puppeteer@21.0.0"]), - json!(&referrer) + json!(&referrer), + json!({ "forceGlobalCache": true }), ]) }), commit_characters: Some( @@ -1112,7 +1520,8 @@ mod tests { command: "deno.cache".to_string(), arguments: Some(vec![ json!(["npm:puppeteer@20.9.0"]), - json!(&referrer) + json!(&referrer), + json!({ "forceGlobalCache": true }), ]) }), commit_characters: Some( diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index 419d08d50a..276cae0a23 100644 --- a/cli/lsp/documents.rs +++ b/cli/lsp/documents.rs @@ -3,7 +3,7 @@ use super::cache::calculate_fs_version; use super::cache::calculate_fs_version_at_path; use super::cache::LSP_DISALLOW_GLOBAL_TO_LOCAL_COPY; -use super::jsr_resolver::JsrResolver; +use super::jsr::JsrResolver; use super::language_server::StateNpmSnapshot; use super::text::LineIndex; use super::tsc; diff --git a/cli/lsp/jsr_resolver.rs b/cli/lsp/jsr.rs similarity index 53% rename from cli/lsp/jsr_resolver.rs rename to cli/lsp/jsr.rs index 4abb0aec55..47a2c1e84f 100644 --- a/cli/lsp/jsr_resolver.rs +++ b/cli/lsp/jsr.rs @@ -1,21 +1,29 @@ // 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 dashmap::DashMap; use deno_cache_dir::HttpCache; +use deno_core::anyhow::anyhow; +use deno_core::error::AnyError; 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::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::sync::Arc; use super::cache::LSP_DISALLOW_GLOBAL_TO_LOCAL_COPY; +use super::search::PackageSearchApi; #[derive(Debug)] pub struct JsrResolver { @@ -58,13 +66,8 @@ impl JsrResolver { } } - pub fn jsr_to_registry_url( - &self, - specifier: &ModuleSpecifier, - ) -> Option { - let req_ref = JsrPackageReqReference::from_str(specifier.as_str()).ok()?; - let req = req_ref.req().clone(); - let maybe_nv = self.nv_by_req.entry(req.clone()).or_insert_with(|| { + pub fn req_to_nv(&self, req: &PackageReq) -> Option { + let nv = self.nv_by_req.entry(req.clone()).or_insert_with(|| { let name = req.name.clone(); let maybe_package_info = self .info_by_name @@ -72,9 +75,11 @@ impl JsrResolver { .or_insert_with(|| read_cached_package_info(&name, &self.cache)); let package_info = maybe_package_info.as_ref()?; // Find the first matching version of the package which is cached. - let version = package_info - .versions - .keys() + 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; @@ -94,6 +99,16 @@ impl JsrResolver { .cloned()?; Some(PackageNv { name, version }) }); + nv.value().clone() + } + + pub fn jsr_to_registry_url( + &self, + specifier: &ModuleSpecifier, + ) -> Option { + let req_ref = JsrPackageReqReference::from_str(specifier.as_str()).ok()?; + let req = req_ref.req().clone(); + let maybe_nv = self.req_to_nv(&req); let nv = maybe_nv.as_ref()?; let maybe_info = self .info_by_nv @@ -169,15 +184,117 @@ fn read_cached_package_version_info( LSP_DISALLOW_GLOBAL_TO_LOCAL_COPY, ) .ok()??; - // This is a roundabout way of deserializing `JsrPackageVersionInfo`, - // because we only want the `exports` field and `module_graph` is large. - let mut info = - serde_json::from_slice::(&meta_bytes).ok()?; - Some(JsrPackageVersionInfo { - manifest: Default::default(), // not used by the LSP (only caching checks this in deno_graph) - exports: info.as_object_mut()?.remove("exports")?, - module_graph: None, - }) + partial_jsr_package_version_info_from_slice(&meta_bytes).ok() +} + +#[derive(Debug, Clone)] +pub struct CliJsrSearchApi { + file_fetcher: FileFetcher, + /// We only store this here so the completion system has access to a resolver + /// that always uses the global cache. + resolver: Arc, + search_cache: Arc>>>, + versions_cache: Arc>>>, + exports_cache: Arc>>>, +} + +impl CliJsrSearchApi { + pub fn new(file_fetcher: FileFetcher) -> Self { + let resolver = Arc::new(JsrResolver::from_cache_and_lockfile( + file_fetcher.http_cache().clone(), + None, + )); + Self { + file_fetcher, + resolver, + search_cache: Default::default(), + versions_cache: Default::default(), + exports_cache: Default::default(), + } + } + + pub fn get_resolver(&self) -> &Arc { + &self.resolver + } +} + +#[async_trait::async_trait] +impl PackageSearchApi for CliJsrSearchApi { + async fn search(&self, query: &str) -> Result>, AnyError> { + if let Some(names) = self.search_cache.get(query) { + return Ok(names.clone()); + } + let mut search_url = jsr_api_url().clone(); + search_url + .path_segments_mut() + .map_err(|_| anyhow!("Custom jsr URL cannot be a base."))? + .pop_if_empty() + .push("packages"); + search_url.query_pairs_mut().append_pair("query", query); + let file = self + .file_fetcher + .fetch(&search_url, PermissionsContainer::allow_all()) + .await? + .into_text_decoded()?; + let names = Arc::new(parse_jsr_search_response(&file.source)?); + self.search_cache.insert(query.to_string(), names.clone()); + Ok(names) + } + + async fn versions(&self, name: &str) -> Result>, AnyError> { + if let Some(versions) = self.versions_cache.get(name) { + return Ok(versions.clone()); + } + let mut meta_url = jsr_url().clone(); + meta_url + .path_segments_mut() + .map_err(|_| anyhow!("Custom jsr URL cannot be a base."))? + .pop_if_empty() + .push(name) + .push("meta.json"); + let file = self + .file_fetcher + .fetch(&meta_url, PermissionsContainer::allow_all()) + .await?; + let info = serde_json::from_slice::(&file.source)?; + let mut versions = info.versions.into_keys().collect::>(); + versions.sort(); + versions.reverse(); + let versions = Arc::new(versions); + self + .versions_cache + .insert(name.to_string(), versions.clone()); + Ok(versions) + } + + async fn exports( + &self, + nv: &PackageNv, + ) -> Result>, AnyError> { + if let Some(exports) = self.exports_cache.get(nv) { + return Ok(exports.clone()); + } + let mut meta_url = jsr_url().clone(); + meta_url + .path_segments_mut() + .map_err(|_| anyhow!("Custom jsr URL cannot be a base."))? + .pop_if_empty() + .push(&nv.name) + .push(&format!("{}_meta.json", &nv.version)); + let file = self + .file_fetcher + .fetch(&meta_url, PermissionsContainer::allow_all()) + .await?; + let info = partial_jsr_package_version_info_from_slice(&file.source)?; + let mut exports = info + .exports() + .map(|(n, _)| n.to_string()) + .collect::>(); + exports.sort(); + let exports = Arc::new(exports); + self.exports_cache.insert(nv.clone(), exports.clone()); + Ok(exports) + } } // TODO(nayeemrmn): This is duplicated from a private function in deno_graph @@ -203,3 +320,42 @@ fn normalize_export_name(sub_path: Option<&str>) -> Cow { } } } + +/// 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( + slice: &[u8], +) -> serde_json::Result { + let mut info = serde_json::from_slice::(slice)?; + Ok(JsrPackageVersionInfo { + manifest: Default::default(), // not used by the LSP (only caching checks this in deno_graph) + exports: info + .as_object_mut() + .and_then(|o| o.remove("exports")) + .unwrap_or_default(), + module_graph: None, + }) +} + +fn parse_jsr_search_response(source: &str) -> Result, AnyError> { + #[derive(Debug, Deserialize)] + #[serde(rename_all = "camelCase")] + struct Item { + scope: String, + name: String, + version_count: usize, + } + #[derive(Debug, Deserialize)] + #[serde(rename_all = "camelCase")] + struct Response { + items: Vec, + } + let items = serde_json::from_str::(source)?.items; + Ok( + items + .into_iter() + .filter(|i| i.version_count > 0) + .map(|i| format!("@{}/{}", i.scope, i.name)) + .collect(), + ) +} diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 45d1eed8a6..4c7a966374 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -23,6 +23,7 @@ use deno_runtime::deno_tls::RootCertStoreProvider; use import_map::ImportMap; use indexmap::IndexSet; use log::error; +use serde::Deserialize; use serde_json::from_value; use std::collections::BTreeMap; use std::collections::HashMap; @@ -69,6 +70,7 @@ use super::documents::Documents; use super::documents::DocumentsFilter; use super::documents::LanguageId; use super::documents::UpdateDocumentConfigOptions; +use super::jsr::CliJsrSearchApi; use super::logging::lsp_log; use super::logging::lsp_warn; use super::lsp_custom; @@ -239,6 +241,7 @@ pub struct Inner { /// on disk or "open" within the client. pub documents: Documents, initial_cwd: PathBuf, + jsr_search_api: CliJsrSearchApi, http_client: Arc, task_queue: LanguageServerTaskQueue, /// Handles module registries, which allow discovery of modules @@ -280,10 +283,11 @@ impl LanguageServer { /// Similar to `deno cache` on the command line, where modules will be cached /// in the Deno cache, including any of their dependencies. - pub async fn cache_request( + pub async fn cache( &self, specifiers: Vec, referrer: ModuleSpecifier, + force_global_cache: bool, ) -> LspResult> { async fn create_graph_for_caching( cli_options: CliOptions, @@ -333,7 +337,7 @@ impl LanguageServer { // do as much as possible in a read, then do a write outside let maybe_prepare_cache_result = { let inner = self.0.read().await; // ensure dropped - match inner.prepare_cache(specifiers, referrer) { + match inner.prepare_cache(specifiers, referrer, force_global_cache) { Ok(maybe_cache_result) => maybe_cache_result, Err(err) => { self @@ -499,13 +503,23 @@ impl Inner { module_registries_location.clone(), http_client.clone(), ); - let npm_search_api = - CliNpmSearchApi::new(module_registries.file_fetcher.clone(), None); let location = dir.deps_folder_path(); let deps_http_cache = Arc::new(GlobalHttpCache::new( location, crate::cache::RealDenoCacheEnv, )); + let mut deps_file_fetcher = FileFetcher::new( + deps_http_cache.clone(), + CacheSetting::RespectHeaders, + true, + http_client.clone(), + Default::default(), + None, + ); + deps_file_fetcher.set_download_log_level(super::logging::lsp_log_level()); + let jsr_search_api = CliJsrSearchApi::new(deps_file_fetcher); + let npm_search_api = + CliNpmSearchApi::new(module_registries.file_fetcher.clone()); let documents = Documents::new(deps_http_cache.clone()); let cache_metadata = cache::CacheMetadata::new(deps_http_cache.clone()); let performance = Arc::new(Performance::default()); @@ -535,6 +549,7 @@ impl Inner { documents, http_client, initial_cwd: initial_cwd.clone(), + jsr_search_api, maybe_global_cache_path: None, maybe_import_map: None, maybe_package_json: None, @@ -832,14 +847,24 @@ impl Inner { module_registries_location.clone(), self.http_client.clone(), ); - self.npm.search_api = - CliNpmSearchApi::new(self.module_registries.file_fetcher.clone(), None); self.module_registries_location = module_registries_location; // update the cache path let global_cache = Arc::new(GlobalHttpCache::new( dir.deps_folder_path(), crate::cache::RealDenoCacheEnv, )); + let mut deps_file_fetcher = FileFetcher::new( + global_cache.clone(), + CacheSetting::RespectHeaders, + true, + self.http_client.clone(), + Default::default(), + None, + ); + deps_file_fetcher.set_download_log_level(super::logging::lsp_log_level()); + self.jsr_search_api = CliJsrSearchApi::new(deps_file_fetcher); + self.npm.search_api = + CliNpmSearchApi::new(self.module_registries.file_fetcher.clone()); let maybe_local_cache = self.config.maybe_vendor_dir_path().map(|local_path| { Arc::new(LocalLspHttpCache::new(local_path, global_cache.clone())) @@ -1040,7 +1065,7 @@ impl Inner { self.task_queue.queue_task(Box::new(|ls: LanguageServer| { spawn(async move { if let Err(err) = - ls.cache_request(specifiers, referrer).await + ls.cache(specifiers, referrer, false).await { lsp_warn!("{}", err); } @@ -2477,6 +2502,7 @@ impl Inner { &self.config.snapshot(), &self.client, &self.module_registries, + &self.jsr_search_api, &self.npm.search_api, &self.documents, self.maybe_import_map.clone(), @@ -3166,14 +3192,20 @@ impl tower_lsp::LanguageServer for LanguageServer { params: ExecuteCommandParams, ) -> LspResult> { if params.command == "deno.cache" { - let mut arguments = params.arguments.into_iter(); - let specifiers = serde_json::to_value(arguments.next()).unwrap(); - let specifiers: Vec = serde_json::from_value(specifiers) - .map_err(|err| LspError::invalid_params(err.to_string()))?; - let referrer = serde_json::to_value(arguments.next()).unwrap(); - let referrer: Url = serde_json::from_value(referrer) - .map_err(|err| LspError::invalid_params(err.to_string()))?; - self.cache_request(specifiers, referrer).await + #[derive(Default, Deserialize)] + #[serde(rename_all = "camelCase")] + struct Options { + #[serde(default)] + force_global_cache: bool, + } + #[derive(Deserialize)] + struct Arguments(Vec, Url, #[serde(default)] Options); + let Arguments(specifiers, referrer, options) = + serde_json::from_value(json!(params.arguments)) + .map_err(|err| LspError::invalid_params(err.to_string()))?; + self + .cache(specifiers, referrer, options.force_global_cache) + .await } else if params.command == "deno.reloadImportRegistries" { self.0.write().await.reload_import_registries().await } else { @@ -3374,7 +3406,7 @@ impl tower_lsp::LanguageServer for LanguageServer { } specifier }; - if let Err(err) = self.cache_request(vec![], specifier.clone()).await { + if let Err(err) = self.cache(vec![], specifier.clone(), false).await { lsp_warn!("Failed to cache \"{}\" on save: {}", &specifier, err); } } @@ -3621,6 +3653,7 @@ impl Inner { &self, specifiers: Vec, referrer: ModuleSpecifier, + force_global_cache: bool, ) -> Result, AnyError> { let mark = self .performance @@ -3650,6 +3683,7 @@ impl Inner { self.config.maybe_config_file().cloned(), self.config.maybe_lockfile().cloned(), self.maybe_package_json.clone(), + force_global_cache, )?; cli_options.set_import_map_specifier( self.maybe_import_map.as_ref().map(|m| m.base_url().clone()), diff --git a/cli/lsp/mod.rs b/cli/lsp/mod.rs index ef64625245..f15d2a3658 100644 --- a/cli/lsp/mod.rs +++ b/cli/lsp/mod.rs @@ -21,7 +21,7 @@ mod completions; mod config; mod diagnostics; mod documents; -mod jsr_resolver; +mod jsr; pub mod language_server; mod logging; mod lsp_custom; @@ -32,6 +32,7 @@ mod performance; mod refactor; mod registries; mod repl; +mod search; mod semantic_tokens; mod testing; mod text; diff --git a/cli/lsp/npm.rs b/cli/lsp/npm.rs index 613b7897e2..59156fe882 100644 --- a/cli/lsp/npm.rs +++ b/cli/lsp/npm.rs @@ -1,56 +1,45 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use std::collections::HashMap; -use std::sync::Arc; - +use dashmap::DashMap; use deno_core::anyhow::anyhow; use deno_core::error::AnyError; -use deno_core::parking_lot::Mutex; use deno_core::serde_json; -use deno_core::url::Url; use deno_npm::registry::NpmPackageInfo; use deno_runtime::permissions::PermissionsContainer; +use deno_semver::package::PackageNv; +use deno_semver::Version; use serde::Deserialize; +use std::sync::Arc; use crate::args::npm_registry_default_url; use crate::file_fetcher::FileFetcher; -#[async_trait::async_trait] -pub trait NpmSearchApi { - async fn search(&self, query: &str) -> Result>, AnyError>; - async fn package_info( - &self, - name: &str, - ) -> Result, AnyError>; -} +use super::search::PackageSearchApi; #[derive(Debug, Clone)] pub struct CliNpmSearchApi { - base_url: Url, file_fetcher: FileFetcher, - info_cache: Arc>>>, - search_cache: Arc>>>>, + search_cache: Arc>>>, + versions_cache: Arc>>>, } impl CliNpmSearchApi { - pub fn new(file_fetcher: FileFetcher, custom_base_url: Option) -> Self { + pub fn new(file_fetcher: FileFetcher) -> Self { Self { - base_url: custom_base_url - .unwrap_or_else(|| npm_registry_default_url().clone()), file_fetcher, - info_cache: Default::default(), search_cache: Default::default(), + versions_cache: Default::default(), } } } #[async_trait::async_trait] -impl NpmSearchApi for CliNpmSearchApi { +impl PackageSearchApi for CliNpmSearchApi { async fn search(&self, query: &str) -> Result>, AnyError> { - if let Some(names) = self.search_cache.lock().get(query) { + if let Some(names) = self.search_cache.get(query) { return Ok(names.clone()); } - let mut search_url = self.base_url.clone(); + let mut search_url = npm_registry_default_url().clone(); search_url .path_segments_mut() .map_err(|_| anyhow!("Custom npm registry URL cannot be a base."))? @@ -65,21 +54,15 @@ impl NpmSearchApi for CliNpmSearchApi { .await? .into_text_decoded()?; let names = Arc::new(parse_npm_search_response(&file.source)?); - self - .search_cache - .lock() - .insert(query.to_string(), names.clone()); + self.search_cache.insert(query.to_string(), names.clone()); Ok(names) } - async fn package_info( - &self, - name: &str, - ) -> Result, AnyError> { - if let Some(info) = self.info_cache.lock().get(name) { - return Ok(info.clone()); + async fn versions(&self, name: &str) -> Result>, AnyError> { + if let Some(versions) = self.versions_cache.get(name) { + return Ok(versions.clone()); } - let mut info_url = self.base_url.clone(); + let mut info_url = npm_registry_default_url().clone(); info_url .path_segments_mut() .map_err(|_| anyhow!("Custom npm registry URL cannot be a base."))? @@ -89,13 +72,22 @@ impl NpmSearchApi for CliNpmSearchApi { .file_fetcher .fetch(&info_url, PermissionsContainer::allow_all()) .await?; - let info = - Arc::new(serde_json::from_slice::(&file.source)?); + let info = serde_json::from_slice::(&file.source)?; + let mut versions = info.versions.into_keys().collect::>(); + versions.sort(); + versions.reverse(); + let versions = Arc::new(versions); self - .info_cache - .lock() - .insert(name.to_string(), info.clone()); - Ok(info) + .versions_cache + .insert(name.to_string(), versions.clone()); + Ok(versions) + } + + async fn exports( + &self, + _nv: &PackageNv, + ) -> Result>, AnyError> { + Ok(Default::default()) } } diff --git a/cli/lsp/search.rs b/cli/lsp/search.rs new file mode 100644 index 0000000000..8933eeb186 --- /dev/null +++ b/cli/lsp/search.rs @@ -0,0 +1,79 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::AnyError; +use deno_semver::package::PackageNv; +use deno_semver::Version; +use std::sync::Arc; + +#[async_trait::async_trait] +pub trait PackageSearchApi { + async fn search(&self, query: &str) -> Result>, AnyError>; + async fn versions(&self, name: &str) -> Result>, AnyError>; + async fn exports(&self, nv: &PackageNv) + -> Result>, AnyError>; +} + +#[cfg(test)] +pub mod tests { + use super::*; + use deno_core::anyhow::anyhow; + use std::collections::BTreeMap; + + #[derive(Debug, Default)] + pub struct TestPackageSearchApi { + /// [(name -> [(version -> [export])])] + package_versions: BTreeMap>>, + } + + impl TestPackageSearchApi { + pub fn with_package_version( + mut self, + name: &str, + version: &str, + exports: &[&str], + ) -> Self { + let exports_by_version = + self.package_versions.entry(name.to_string()).or_default(); + exports_by_version.insert( + Version::parse_standard(version).unwrap(), + exports.iter().map(|s| s.to_string()).collect(), + ); + self + } + } + + #[async_trait::async_trait] + impl PackageSearchApi for TestPackageSearchApi { + async fn search(&self, query: &str) -> Result>, AnyError> { + let names = self + .package_versions + .keys() + .filter_map(|n| n.contains(query).then(|| n.clone())) + .collect::>(); + Ok(Arc::new(names)) + } + + async fn versions( + &self, + name: &str, + ) -> Result>, AnyError> { + let Some(exports_by_version) = self.package_versions.get(name) else { + return Err(anyhow!("Package not found.")); + }; + Ok(Arc::new(exports_by_version.keys().rev().cloned().collect())) + } + + async fn exports( + &self, + nv: &PackageNv, + ) -> Result>, AnyError> { + let Some(exports_by_version) = self.package_versions.get(&nv.name) else { + return Err(anyhow!("Package not found.")); + }; + let Some(exports) = exports_by_version.get(&nv.version) else { + return Err(anyhow!("Package version not found.")); + }; + Ok(Arc::new(exports.clone())) + } + } +}