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

feat(lsp): auto-import completions for jsr specifiers (#22462)

This commit is contained in:
Nayeem Rahman 2024-02-21 02:45:00 +00:00 committed by GitHub
parent 77b90f408c
commit e32c704970
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 248 additions and 4 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -4831,6 +4831,154 @@ fn lsp_jsr_lockfile() {
client.shutdown();
}
#[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();