summaryrefslogtreecommitdiff
path: root/cli/lsp/npm.rs
diff options
context:
space:
mode:
authorNayeem Rahman <nayeemrmn99@gmail.com>2023-08-29 16:22:05 +0100
committerGitHub <noreply@github.com>2023-08-29 10:22:05 -0500
commitb5f032df73bd5de78be53f002b9bef048cf59f44 (patch)
treea462153fd3ccea4fc01fbb4840e801f50b8b2913 /cli/lsp/npm.rs
parent2929313652a9d7fb3a26ad7f754deaeb03435e10 (diff)
feat(lsp): npm specifier completions (#20121)
Diffstat (limited to 'cli/lsp/npm.rs')
-rw-r--r--cli/lsp/npm.rs136
1 files changed, 136 insertions, 0 deletions
diff --git a/cli/lsp/npm.rs b/cli/lsp/npm.rs
new file mode 100644
index 000000000..0f2794e44
--- /dev/null
+++ b/cli/lsp/npm.rs
@@ -0,0 +1,136 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+use std::collections::HashMap;
+use std::sync::Arc;
+
+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 serde::Deserialize;
+
+use crate::file_fetcher::FileFetcher;
+use crate::npm::CliNpmRegistryApi;
+
+#[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>;
+}
+
+#[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>>>>>,
+}
+
+impl CliNpmSearchApi {
+ pub fn new(file_fetcher: FileFetcher, custom_base_url: Option<Url>) -> Self {
+ Self {
+ base_url: custom_base_url
+ .unwrap_or_else(|| CliNpmRegistryApi::default_url().clone()),
+ file_fetcher,
+ info_cache: Default::default(),
+ search_cache: Default::default(),
+ }
+ }
+}
+
+#[async_trait::async_trait]
+impl NpmSearchApi for CliNpmSearchApi {
+ async fn search(&self, query: &str) -> Result<Arc<Vec<String>>, AnyError> {
+ if let Some(names) = self.search_cache.lock().get(query) {
+ return Ok(names.clone());
+ }
+ let mut search_url = self.base_url.clone();
+ search_url
+ .path_segments_mut()
+ .map_err(|_| anyhow!("Custom npm registry URL cannot be a base."))?
+ .pop_if_empty()
+ .extend("-/v1/search".split('/'));
+ search_url
+ .query_pairs_mut()
+ .append_pair("text", &format!("{} boost-exact:false", query));
+ let file = self
+ .file_fetcher
+ .fetch(&search_url, PermissionsContainer::allow_all())
+ .await?;
+ let names = Arc::new(parse_npm_search_response(&file.source)?);
+ self
+ .search_cache
+ .lock()
+ .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());
+ }
+ let mut info_url = self.base_url.clone();
+ info_url
+ .path_segments_mut()
+ .map_err(|_| anyhow!("Custom npm registry URL cannot be a base."))?
+ .pop_if_empty()
+ .push(name);
+ let file = self
+ .file_fetcher
+ .fetch(&info_url, PermissionsContainer::allow_all())
+ .await?;
+ let info = Arc::new(serde_json::from_str::<NpmPackageInfo>(&file.source)?);
+ self
+ .info_cache
+ .lock()
+ .insert(name.to_string(), info.clone());
+ Ok(info)
+ }
+}
+
+fn parse_npm_search_response(source: &str) -> Result<Vec<String>, AnyError> {
+ #[derive(Debug, Deserialize)]
+ struct Package {
+ name: String,
+ }
+ #[derive(Debug, Deserialize)]
+ struct Object {
+ package: Package,
+ }
+ #[derive(Debug, Deserialize)]
+ struct Response {
+ objects: Vec<Object>,
+ }
+ let objects = serde_json::from_str::<Response>(source)?.objects;
+ Ok(objects.into_iter().map(|o| o.package.name).collect())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_npm_search_response() {
+ // This is a subset of a realistic response only containing data currently
+ // used by our parser. It's enough to catch regressions.
+ let names = parse_npm_search_response(r#"{"objects":[{"package":{"name":"puppeteer"}},{"package":{"name":"puppeteer-core"}},{"package":{"name":"puppeteer-extra-plugin-stealth"}},{"package":{"name":"puppeteer-extra-plugin"}}]}"#).unwrap();
+ assert_eq!(
+ names,
+ vec![
+ "puppeteer".to_string(),
+ "puppeteer-core".to_string(),
+ "puppeteer-extra-plugin-stealth".to_string(),
+ "puppeteer-extra-plugin".to_string()
+ ]
+ );
+ }
+}