summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNayeem Rahman <nayeemrmn99@gmail.com>2024-02-21 02:45:00 +0000
committerGitHub <noreply@github.com>2024-02-21 02:45:00 +0000
commite32c704970d9c332367757cbd21f1905c2d11486 (patch)
tree410c36122a4b0cba64721db52734e7ba94569cff
parent77b90f408c4244e8ee2e4b3bd26c441d4a250671 (diff)
feat(lsp): auto-import completions for jsr specifiers (#22462)
-rw-r--r--cli/lsp/analysis.rs58
-rw-r--r--cli/lsp/documents.rs4
-rw-r--r--cli/lsp/jsr_resolver.rs31
-rw-r--r--cli/lsp/tsc.rs11
-rw-r--r--tests/integration/lsp_tests.rs148
5 files changed, 248 insertions, 4 deletions
diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs
index 96ee422c6..3dd78e428 100644
--- a/cli/lsp/analysis.rs
+++ b/cli/lsp/analysis.rs
@@ -6,6 +6,7 @@ use super::documents::Documents;
use super::language_server;
use super::tsc;
+use crate::args::jsr_url;
use crate::npm::CliNpmResolver;
use crate::tools::lint::create_linter;
use crate::util::path::specifier_to_file_path;
@@ -26,8 +27,14 @@ use deno_runtime::deno_node::NodeResolver;
use deno_runtime::deno_node::NpmResolver;
use deno_runtime::deno_node::PathClean;
use deno_runtime::permissions::PermissionsContainer;
+use deno_semver::jsr::JsrPackageNvReference;
+use deno_semver::jsr::JsrPackageReqReference;
use deno_semver::npm::NpmPackageReqReference;
+use deno_semver::package::PackageNv;
+use deno_semver::package::PackageNvReference;
use deno_semver::package::PackageReq;
+use deno_semver::package::PackageReqReference;
+use deno_semver::Version;
use import_map::ImportMap;
use once_cell::sync::Lazy;
use regex::Regex;
@@ -208,6 +215,57 @@ impl<'a> TsResponseImportMapper<'a> {
}
}
+ if let Some(jsr_path) = specifier.as_str().strip_prefix(jsr_url().as_str())
+ {
+ let mut segments = jsr_path.split('/');
+ let name = if jsr_path.starts_with('@') {
+ format!("{}/{}", segments.next()?, segments.next()?)
+ } else {
+ segments.next()?.to_string()
+ };
+ let version = Version::parse_standard(segments.next()?).ok()?;
+ let nv = PackageNv { name, version };
+ let path = segments.collect::<Vec<_>>().join("/");
+ let jsr_resolver = self.documents.get_jsr_resolver();
+ let export = jsr_resolver.lookup_export_for_path(&nv, &path)?;
+ let sub_path = (export != ".").then_some(export);
+ let mut req = None;
+ req = req.or_else(|| {
+ let import_map = self.maybe_import_map?;
+ for entry in import_map.entries_for_referrer(referrer) {
+ let Some(value) = entry.raw_value else {
+ continue;
+ };
+ let Ok(req_ref) = JsrPackageReqReference::from_str(value) else {
+ continue;
+ };
+ let req = req_ref.req();
+ if req.name == nv.name
+ && req.version_req.tag().is_none()
+ && req.version_req.matches(&nv.version)
+ {
+ return Some(req.clone());
+ }
+ }
+ None
+ });
+ req = req.or_else(|| jsr_resolver.lookup_req_for_nv(&nv));
+ let spec_str = if let Some(req) = req {
+ let req_ref = PackageReqReference { req, sub_path };
+ JsrPackageReqReference::new(req_ref).to_string()
+ } else {
+ let nv_ref = PackageNvReference { nv, sub_path };
+ JsrPackageNvReference::new(nv_ref).to_string()
+ };
+ let specifier = ModuleSpecifier::parse(&spec_str).ok()?;
+ if let Some(import_map) = self.maybe_import_map {
+ if let Some(result) = import_map.lookup(&specifier, referrer) {
+ return Some(result);
+ }
+ }
+ return Some(spec_str);
+ }
+
if let Some(npm_resolver) =
self.npm_resolver.as_ref().and_then(|r| r.as_managed())
{
diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs
index c727d0fc9..ddff92342 100644
--- a/cli/lsp/documents.rs
+++ b/cli/lsp/documents.rs
@@ -1332,6 +1332,10 @@ impl Documents {
Ok(())
}
+ pub fn get_jsr_resolver(&self) -> &Arc<JsrResolver> {
+ &self.jsr_resolver
+ }
+
pub fn refresh_jsr_resolver(
&mut self,
lockfile: Option<Arc<Mutex<Lockfile>>>,
diff --git a/cli/lsp/jsr_resolver.rs b/cli/lsp/jsr_resolver.rs
index be7bdc0f5..4abb0aec5 100644
--- a/cli/lsp/jsr_resolver.rs
+++ b/cli/lsp/jsr_resolver.rs
@@ -105,6 +105,37 @@ impl JsrResolver {
.join(&format!("{}/{}/{}", &nv.name, &nv.version, &path))
.ok()
}
+
+ pub fn lookup_export_for_path(
+ &self,
+ nv: &PackageNv,
+ path: &str,
+ ) -> Option<String> {
+ let maybe_info = self
+ .info_by_nv
+ .entry(nv.clone())
+ .or_insert_with(|| read_cached_package_version_info(nv, &self.cache));
+ let info = maybe_info.as_ref()?;
+ let path = path.strip_prefix("./").unwrap_or(path);
+ for (export, path_) in info.exports() {
+ if path_.strip_prefix("./").unwrap_or(path_) == path {
+ return Some(export.strip_prefix("./").unwrap_or(export).to_string());
+ }
+ }
+ None
+ }
+
+ pub fn lookup_req_for_nv(&self, nv: &PackageNv) -> Option<PackageReq> {
+ for entry in self.nv_by_req.iter() {
+ let Some(nv_) = entry.value() else {
+ continue;
+ };
+ if nv_ == nv {
+ return Some(entry.key().clone());
+ }
+ }
+ None
+ }
}
fn read_cached_package_info(
diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs
index eeb07e003..138640d7f 100644
--- a/cli/lsp/tsc.rs
+++ b/cli/lsp/tsc.rs
@@ -20,6 +20,7 @@ use super::urls::LspClientUrl;
use super::urls::LspUrlMap;
use super::urls::INVALID_SPECIFIER;
+use crate::args::jsr_url;
use crate::args::FmtOptionsConfig;
use crate::args::TsConfig;
use crate::cache::HttpCache;
@@ -3228,7 +3229,7 @@ impl CompletionInfo {
let items = self
.entries
.iter()
- .map(|entry| {
+ .flat_map(|entry| {
entry.as_completion_item(
line_index.clone(),
self,
@@ -3405,7 +3406,7 @@ impl CompletionEntry {
specifier: &ModuleSpecifier,
position: u32,
language_server: &language_server::Inner,
- ) -> lsp::CompletionItem {
+ ) -> Option<lsp::CompletionItem> {
let mut label = self.name.clone();
let mut label_details: Option<lsp::CompletionItemLabelDetails> = None;
let mut kind: Option<lsp::CompletionItemKind> =
@@ -3481,6 +3482,8 @@ impl CompletionEntry {
specifier_rewrite =
Some((import_data.module_specifier, new_module_specifier));
}
+ } else if source.starts_with(jsr_url().as_str()) {
+ return None;
}
}
}
@@ -3520,7 +3523,7 @@ impl CompletionEntry {
use_code_snippet,
};
- lsp::CompletionItem {
+ Some(lsp::CompletionItem {
label,
label_details,
kind,
@@ -3535,7 +3538,7 @@ impl CompletionEntry {
commit_characters,
data: Some(json!({ "tsc": tsc })),
..Default::default()
- }
+ })
}
}
diff --git a/tests/integration/lsp_tests.rs b/tests/integration/lsp_tests.rs
index 34ea7362c..3ae738111 100644
--- a/tests/integration/lsp_tests.rs
+++ b/tests/integration/lsp_tests.rs
@@ -4832,6 +4832,154 @@ fn lsp_jsr_lockfile() {
}
#[test]
+fn lsp_jsr_auto_import_completion() {
+ let context = TestContextBuilder::new()
+ .use_http_server()
+ .use_temp_cwd()
+ .build();
+ let temp_dir = context.temp_dir();
+ temp_dir.write(
+ "main.ts",
+ r#"
+ import "jsr:@denotest/add@1";
+ "#,
+ );
+ let mut client = context.new_lsp_command().build();
+ client.initialize_default();
+ client.write_request(
+ "workspace/executeCommand",
+ json!({
+ "command": "deno.cache",
+ "arguments": [
+ [],
+ temp_dir.uri().join("main.ts").unwrap(),
+ ],
+ }),
+ );
+ client.did_open(json!({
+ "textDocument": {
+ "uri": temp_dir.uri().join("file.ts").unwrap(),
+ "languageId": "typescript",
+ "version": 1,
+ "text": r#"add"#,
+ }
+ }));
+ let list = client.get_completion_list(
+ temp_dir.uri().join("file.ts").unwrap(),
+ (0, 3),
+ json!({ "triggerKind": 1 }),
+ );
+ assert!(!list.is_incomplete);
+ assert_eq!(list.items.len(), 261);
+ let item = list.items.iter().find(|i| i.label == "add").unwrap();
+ assert_eq!(&item.label, "add");
+ assert_eq!(
+ json!(&item.label_details),
+ json!({ "description": "jsr:@denotest/add@1" })
+ );
+
+ let res = client.write_request("completionItem/resolve", json!(item));
+ assert_eq!(
+ res,
+ json!({
+ "label": "add",
+ "labelDetails": { "description": "jsr:@denotest/add@1" },
+ "kind": 3,
+ "detail": "function add(a: number, b: number): number",
+ "documentation": { "kind": "markdown", "value": "" },
+ "sortText": "\u{ffff}16_1",
+ "additionalTextEdits": [
+ {
+ "range": {
+ "start": { "line": 0, "character": 0 },
+ "end": { "line": 0, "character": 0 },
+ },
+ "newText": "import { add } from \"jsr:@denotest/add@1\";\n\n",
+ },
+ ],
+ })
+ );
+ client.shutdown();
+}
+
+#[test]
+fn lsp_jsr_auto_import_completion_import_map() {
+ let context = TestContextBuilder::new()
+ .use_http_server()
+ .use_temp_cwd()
+ .build();
+ let temp_dir = context.temp_dir();
+ temp_dir.write(
+ "deno.json",
+ json!({
+ "imports": {
+ "add": "jsr:@denotest/add@^1.0",
+ },
+ })
+ .to_string(),
+ );
+ temp_dir.write(
+ "main.ts",
+ r#"
+ import "jsr:@denotest/add@1";
+ "#,
+ );
+ let mut client = context.new_lsp_command().build();
+ client.initialize_default();
+ client.write_request(
+ "workspace/executeCommand",
+ json!({
+ "command": "deno.cache",
+ "arguments": [
+ [],
+ temp_dir.uri().join("main.ts").unwrap(),
+ ],
+ }),
+ );
+ client.did_open(json!({
+ "textDocument": {
+ "uri": temp_dir.uri().join("file.ts").unwrap(),
+ "languageId": "typescript",
+ "version": 1,
+ "text": r#"add"#,
+ }
+ }));
+ let list = client.get_completion_list(
+ temp_dir.uri().join("file.ts").unwrap(),
+ (0, 3),
+ json!({ "triggerKind": 1 }),
+ );
+ assert!(!list.is_incomplete);
+ assert_eq!(list.items.len(), 261);
+ let item = list.items.iter().find(|i| i.label == "add").unwrap();
+ assert_eq!(&item.label, "add");
+ assert_eq!(json!(&item.label_details), json!({ "description": "add" }));
+
+ let res = client.write_request("completionItem/resolve", json!(item));
+ assert_eq!(
+ res,
+ json!({
+ "label": "add",
+ "labelDetails": { "description": "add" },
+ "kind": 3,
+ "detail": "function add(a: number, b: number): number",
+ "documentation": { "kind": "markdown", "value": "" },
+ "sortText": "\u{ffff}16_0",
+ "additionalTextEdits": [
+ {
+ "range": {
+ "start": { "line": 0, "character": 0 },
+ "end": { "line": 0, "character": 0 },
+ },
+ "newText": "import { add } from \"add\";\n\n",
+ },
+ ],
+ })
+ );
+ client.shutdown();
+}
+
+#[test]
fn lsp_code_actions_deno_cache_npm() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();