1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-21 15:04:11 -05:00

feat(lsp): jsr specifier completions (#22612)

This commit is contained in:
Nayeem Rahman 2024-02-29 03:54:16 +00:00 committed by GitHub
parent 814eb42060
commit 3a43568481
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 891 additions and 210 deletions

View file

@ -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,
)
}

View file

@ -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
}

View file

@ -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_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(

View file

@ -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;

View file

@ -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(),
)
}

View file

@ -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()),

View file

@ -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;

View file

@ -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())
}
}

79
cli/lsp/search.rs Normal file
View file

@ -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()))
}
}
}