summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNayeem Rahman <nayeemrmn99@gmail.com>2024-02-29 03:54:16 +0000
committerGitHub <noreply@github.com>2024-02-29 03:54:16 +0000
commit3a43568481108545178f5dd2f928d40736dc987a (patch)
tree5c9deec7135b5c9ace35aa2c13b238b3fdb53bb1
parent814eb420608b21d3d79d3580317d2e803d240589 (diff)
feat(lsp): jsr specifier completions (#22612)
-rw-r--r--cli/args/mod.rs10
-rw-r--r--cli/file_fetcher.rs4
-rw-r--r--cli/lsp/completions.rs639
-rw-r--r--cli/lsp/documents.rs2
-rw-r--r--cli/lsp/jsr.rs (renamed from cli/lsp/jsr_resolver.rs)194
-rw-r--r--cli/lsp/language_server.rs66
-rw-r--r--cli/lsp/mod.rs3
-rw-r--r--cli/lsp/npm.rs72
-rw-r--r--cli/lsp/search.rs79
9 files changed, 875 insertions, 194 deletions
diff --git a/cli/args/mod.rs b/cli/args/mod.rs
index 7fcc56c5f..95df047e6 100644
--- a/cli/args/mod.rs
+++ b/cli/args/mod.rs
@@ -693,6 +693,7 @@ impl CliOptions {
maybe_config_file: Option<ConfigFile>,
maybe_lockfile: Option<Arc<Mutex<Lockfile>>>,
maybe_package_json: Option<PackageJson>,
+ force_global_cache: bool,
) -> Result<Self, AnyError> {
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 a74a14a3f..5b530cf6d 100644
--- a/cli/file_fetcher.rs
+++ b/cli/file_fetcher.rs
@@ -186,6 +186,10 @@ impl FileFetcher {
}
}
+ pub fn http_cache(&self) -> &Arc<dyn HttpCache> {
+ &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 7fe161039..074d913c5 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<Arc<ImportMap>>,
@@ -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<usize> {
- let bare_specifier = specifier.strip_prefix("npm:")?;
+fn parse_bare_specifier_version_index(bare_specifier: &str) -> Option<usize> {
if bare_specifier.starts_with('@') {
bare_specifier
.find('/')
@@ -486,49 +503,188 @@ fn parse_npm_specifier_version_index(specifier: &str) -> Option<usize> {
.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<Vec<lsp::CompletionItem>> {
+ // First try to match `jsr:some-package@some-version/<export-to-complete>`.
+ 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@<version-to-complete>`.
+ 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:<package-to-complete>`.
+ 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<Vec<lsp::CompletionItem>> {
// First try to match `npm:some-package@<version-to-complete>`.
- 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::<Vec<_>>();
- 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:<package-to-complete>`.
- 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<String, Arc<Vec<String>>>,
- TestNpmRegistryApi,
- );
-
- #[async_trait]
- impl NpmSearchApi for TestNpmSearchApi {
- async fn search(&self, query: &str) -> Result<Arc<Vec<String>>, AnyError> {
- match self.0.get(query) {
- Some(names) => Ok(names.clone()),
- None => Ok(Arc::new(vec![])),
- }
- }
-
- async fn package_info(
- &self,
- name: &str,
- ) -> Result<Arc<NpmPackageInfo>, 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_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(),
+ 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 = 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,7 +1353,8 @@ mod tests {
command: "deno.cache".to_string(),
arguments: Some(vec![
json!(["npm:puppeteer-core"]),
- json!(&referrer)
+ json!(&referrer),
+ json!({ "forceGlobalCache": true }),
])
}),
commit_characters: Some(
@@ -952,20 +1363,21 @@ mod tests {
..Default::default()
},
lsp::CompletionItem {
- label: "npm:puppeteer-extra-plugin-stealth".to_string(),
+ label: "npm:puppeteer-extra-plugin".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(),
+ new_text: "npm:puppeteer-extra-plugin".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!(["npm:puppeteer-extra-plugin"]),
+ json!(&referrer),
+ json!({ "forceGlobalCache": true }),
])
}),
commit_characters: Some(
@@ -974,20 +1386,21 @@ mod tests {
..Default::default()
},
lsp::CompletionItem {
- label: "npm:puppeteer-extra-plugin".to_string(),
+ 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".to_string(),
+ 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"]),
- json!(&referrer)
+ 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 419d08d50..276cae0a2 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
index 4abb0aec5..47a2c1e84 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<ModuleSpecifier> {
- 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<PackageNv> {
+ 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::<Vec<_>>();
+ 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<ModuleSpecifier> {
+ 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::<serde_json::Value>(&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<JsrResolver>,
+ search_cache: Arc<DashMap<String, Arc<Vec<String>>>>,
+ versions_cache: Arc<DashMap<String, Arc<Vec<Version>>>>,
+ exports_cache: Arc<DashMap<PackageNv, Arc<Vec<String>>>>,
+}
+
+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<JsrResolver> {
+ &self.resolver
+ }
+}
+
+#[async_trait::async_trait]
+impl PackageSearchApi for CliJsrSearchApi {
+ async fn search(&self, query: &str) -> Result<Arc<Vec<String>>, 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<Arc<Vec<Version>>, 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::<JsrPackageInfo>(&file.source)?;
+ let mut versions = info.versions.into_keys().collect::<Vec<_>>();
+ 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<Arc<Vec<String>>, 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::<Vec<_>>();
+ 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<str> {
}
}
}
+
+/// 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<JsrPackageVersionInfo> {
+ let mut info = serde_json::from_slice::<serde_json::Value>(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<Vec<String>, 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<Item>,
+ }
+ let items = serde_json::from_str::<Response>(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 45d1eed8a..4c7a96637 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<HttpClient>,
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<ModuleSpecifier>,
referrer: ModuleSpecifier,
+ force_global_cache: bool,
) -> LspResult<Option<Value>> {
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<Option<Value>> {
if params.command == "deno.cache" {
- let mut arguments = params.arguments.into_iter();
- let specifiers = serde_json::to_value(arguments.next()).unwrap();
- let specifiers: Vec<Url> = 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>, 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<ModuleSpecifier>,
referrer: ModuleSpecifier,
+ force_global_cache: bool,
) -> Result<Option<PrepareCacheResult>, 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 ef6462524..f15d2a365 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 613b7897e..59156fe88 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<Arc<Vec<String>>, AnyError>;
- async fn package_info(
- &self,
- name: &str,
- ) -> Result<Arc<NpmPackageInfo>, AnyError>;
-}
+use super::search::PackageSearchApi;
#[derive(Debug, Clone)]
pub struct CliNpmSearchApi {
- base_url: Url,
file_fetcher: FileFetcher,
- info_cache: Arc<Mutex<HashMap<String, Arc<NpmPackageInfo>>>>,
- search_cache: Arc<Mutex<HashMap<String, Arc<Vec<String>>>>>,
+ search_cache: Arc<DashMap<String, Arc<Vec<String>>>>,
+ versions_cache: Arc<DashMap<String, Arc<Vec<Version>>>>,
}
impl CliNpmSearchApi {
- pub fn new(file_fetcher: FileFetcher, custom_base_url: Option<Url>) -> 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<Arc<Vec<String>>, 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<Arc<NpmPackageInfo>, AnyError> {
- if let Some(info) = self.info_cache.lock().get(name) {
- return Ok(info.clone());
+ async fn versions(&self, name: &str) -> Result<Arc<Vec<Version>>, 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::<NpmPackageInfo>(&file.source)?);
+ let info = serde_json::from_slice::<NpmPackageInfo>(&file.source)?;
+ let mut versions = info.versions.into_keys().collect::<Vec<_>>();
+ 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<Arc<Vec<String>>, AnyError> {
+ Ok(Default::default())
}
}
diff --git a/cli/lsp/search.rs b/cli/lsp/search.rs
new file mode 100644
index 000000000..8933eeb18
--- /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<Arc<Vec<String>>, AnyError>;
+ async fn versions(&self, name: &str) -> Result<Arc<Vec<Version>>, AnyError>;
+ async fn exports(&self, nv: &PackageNv)
+ -> Result<Arc<Vec<String>>, 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<String, BTreeMap<Version, Vec<String>>>,
+ }
+
+ 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<Arc<Vec<String>>, AnyError> {
+ let names = self
+ .package_versions
+ .keys()
+ .filter_map(|n| n.contains(query).then(|| n.clone()))
+ .collect::<Vec<_>>();
+ Ok(Arc::new(names))
+ }
+
+ async fn versions(
+ &self,
+ name: &str,
+ ) -> Result<Arc<Vec<Version>>, 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<Arc<Vec<String>>, 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()))
+ }
+ }
+}