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:
parent
814eb42060
commit
3a43568481
9 changed files with 891 additions and 210 deletions
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
|
@ -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()),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
79
cli/lsp/search.rs
Normal 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()))
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue