mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 17:34:47 -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::language_server;
|
||||||
use super::tsc;
|
use super::tsc;
|
||||||
|
|
||||||
|
use crate::args::jsr_url;
|
||||||
use crate::npm::CliNpmResolver;
|
use crate::npm::CliNpmResolver;
|
||||||
use crate::tools::lint::create_linter;
|
use crate::tools::lint::create_linter;
|
||||||
use crate::util::path::specifier_to_file_path;
|
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::NpmResolver;
|
||||||
use deno_runtime::deno_node::PathClean;
|
use deno_runtime::deno_node::PathClean;
|
||||||
use deno_runtime::permissions::PermissionsContainer;
|
use deno_runtime::permissions::PermissionsContainer;
|
||||||
|
use deno_semver::jsr::JsrPackageNvReference;
|
||||||
|
use deno_semver::jsr::JsrPackageReqReference;
|
||||||
use deno_semver::npm::NpmPackageReqReference;
|
use deno_semver::npm::NpmPackageReqReference;
|
||||||
|
use deno_semver::package::PackageNv;
|
||||||
|
use deno_semver::package::PackageNvReference;
|
||||||
use deno_semver::package::PackageReq;
|
use deno_semver::package::PackageReq;
|
||||||
|
use deno_semver::package::PackageReqReference;
|
||||||
|
use deno_semver::Version;
|
||||||
use import_map::ImportMap;
|
use import_map::ImportMap;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
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) =
|
if let Some(npm_resolver) =
|
||||||
self.npm_resolver.as_ref().and_then(|r| r.as_managed())
|
self.npm_resolver.as_ref().and_then(|r| r.as_managed())
|
||||||
{
|
{
|
||||||
|
|
|
@ -1332,6 +1332,10 @@ impl Documents {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_jsr_resolver(&self) -> &Arc<JsrResolver> {
|
||||||
|
&self.jsr_resolver
|
||||||
|
}
|
||||||
|
|
||||||
pub fn refresh_jsr_resolver(
|
pub fn refresh_jsr_resolver(
|
||||||
&mut self,
|
&mut self,
|
||||||
lockfile: Option<Arc<Mutex<Lockfile>>>,
|
lockfile: Option<Arc<Mutex<Lockfile>>>,
|
||||||
|
|
|
@ -105,6 +105,37 @@ impl JsrResolver {
|
||||||
.join(&format!("{}/{}/{}", &nv.name, &nv.version, &path))
|
.join(&format!("{}/{}/{}", &nv.name, &nv.version, &path))
|
||||||
.ok()
|
.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(
|
fn read_cached_package_info(
|
||||||
|
|
|
@ -20,6 +20,7 @@ use super::urls::LspClientUrl;
|
||||||
use super::urls::LspUrlMap;
|
use super::urls::LspUrlMap;
|
||||||
use super::urls::INVALID_SPECIFIER;
|
use super::urls::INVALID_SPECIFIER;
|
||||||
|
|
||||||
|
use crate::args::jsr_url;
|
||||||
use crate::args::FmtOptionsConfig;
|
use crate::args::FmtOptionsConfig;
|
||||||
use crate::args::TsConfig;
|
use crate::args::TsConfig;
|
||||||
use crate::cache::HttpCache;
|
use crate::cache::HttpCache;
|
||||||
|
@ -3228,7 +3229,7 @@ impl CompletionInfo {
|
||||||
let items = self
|
let items = self
|
||||||
.entries
|
.entries
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| {
|
.flat_map(|entry| {
|
||||||
entry.as_completion_item(
|
entry.as_completion_item(
|
||||||
line_index.clone(),
|
line_index.clone(),
|
||||||
self,
|
self,
|
||||||
|
@ -3405,7 +3406,7 @@ impl CompletionEntry {
|
||||||
specifier: &ModuleSpecifier,
|
specifier: &ModuleSpecifier,
|
||||||
position: u32,
|
position: u32,
|
||||||
language_server: &language_server::Inner,
|
language_server: &language_server::Inner,
|
||||||
) -> lsp::CompletionItem {
|
) -> Option<lsp::CompletionItem> {
|
||||||
let mut label = self.name.clone();
|
let mut label = self.name.clone();
|
||||||
let mut label_details: Option<lsp::CompletionItemLabelDetails> = None;
|
let mut label_details: Option<lsp::CompletionItemLabelDetails> = None;
|
||||||
let mut kind: Option<lsp::CompletionItemKind> =
|
let mut kind: Option<lsp::CompletionItemKind> =
|
||||||
|
@ -3481,6 +3482,8 @@ impl CompletionEntry {
|
||||||
specifier_rewrite =
|
specifier_rewrite =
|
||||||
Some((import_data.module_specifier, new_module_specifier));
|
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,
|
use_code_snippet,
|
||||||
};
|
};
|
||||||
|
|
||||||
lsp::CompletionItem {
|
Some(lsp::CompletionItem {
|
||||||
label,
|
label,
|
||||||
label_details,
|
label_details,
|
||||||
kind,
|
kind,
|
||||||
|
@ -3535,7 +3538,7 @@ impl CompletionEntry {
|
||||||
commit_characters,
|
commit_characters,
|
||||||
data: Some(json!({ "tsc": tsc })),
|
data: Some(json!({ "tsc": tsc })),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4831,6 +4831,154 @@ fn lsp_jsr_lockfile() {
|
||||||
client.shutdown();
|
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]
|
#[test]
|
||||||
fn lsp_code_actions_deno_cache_npm() {
|
fn lsp_code_actions_deno_cache_npm() {
|
||||||
let context = TestContextBuilder::new().use_temp_cwd().build();
|
let context = TestContextBuilder::new().use_temp_cwd().build();
|
||||||
|
|
Loading…
Add table
Reference in a new issue