diff options
author | Nayeem Rahman <nayeemrmn99@gmail.com> | 2024-02-21 02:45:00 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-21 02:45:00 +0000 |
commit | e32c704970d9c332367757cbd21f1905c2d11486 (patch) | |
tree | 410c36122a4b0cba64721db52734e7ba94569cff | |
parent | 77b90f408c4244e8ee2e4b3bd26c441d4a250671 (diff) |
feat(lsp): auto-import completions for jsr specifiers (#22462)
-rw-r--r-- | cli/lsp/analysis.rs | 58 | ||||
-rw-r--r-- | cli/lsp/documents.rs | 4 | ||||
-rw-r--r-- | cli/lsp/jsr_resolver.rs | 31 | ||||
-rw-r--r-- | cli/lsp/tsc.rs | 11 | ||||
-rw-r--r-- | tests/integration/lsp_tests.rs | 148 |
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(); |