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:
parent
77b90f408c
commit
e32c704970
5 changed files with 248 additions and 4 deletions
|
@ -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())
|
||||
{
|
||||
|
|
|
@ -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>>>,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue