mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 09:31:22 -05:00
feat(lsp): support import maps in quick fix and auto-imports (#19692)
Closes https://github.com/denoland/vscode_deno/issues/849 Closes #15330 Closes #10951 Closes #13623
This commit is contained in:
parent
2c2e6adae8
commit
e8a866ca8a
4 changed files with 449 additions and 19 deletions
|
@ -22,6 +22,8 @@ use deno_core::ModuleSpecifier;
|
|||
use deno_lint::rules::LintRule;
|
||||
use deno_runtime::deno_node::PackageJson;
|
||||
use deno_runtime::deno_node::PathClean;
|
||||
use deno_semver::npm::NpmPackageReq;
|
||||
use import_map::ImportMap;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use std::cmp::Ordering;
|
||||
|
@ -157,6 +159,7 @@ fn code_as_string(code: &Option<lsp::NumberOrString>) -> String {
|
|||
/// Rewrites imports in quick fixes and code changes to be Deno specific.
|
||||
pub struct TsResponseImportMapper<'a> {
|
||||
documents: &'a Documents,
|
||||
maybe_import_map: Option<&'a ImportMap>,
|
||||
npm_resolution: &'a NpmResolution,
|
||||
npm_resolver: &'a CliNpmResolver,
|
||||
}
|
||||
|
@ -164,37 +167,81 @@ pub struct TsResponseImportMapper<'a> {
|
|||
impl<'a> TsResponseImportMapper<'a> {
|
||||
pub fn new(
|
||||
documents: &'a Documents,
|
||||
maybe_import_map: Option<&'a ImportMap>,
|
||||
npm_resolution: &'a NpmResolution,
|
||||
npm_resolver: &'a CliNpmResolver,
|
||||
) -> Self {
|
||||
Self {
|
||||
documents,
|
||||
maybe_import_map,
|
||||
npm_resolution,
|
||||
npm_resolver,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_specifier(&self, specifier: &ModuleSpecifier) -> Option<String> {
|
||||
pub fn check_specifier(
|
||||
&self,
|
||||
specifier: &ModuleSpecifier,
|
||||
referrer: &ModuleSpecifier,
|
||||
) -> Option<String> {
|
||||
fn concat_npm_specifier(
|
||||
prefix: &str,
|
||||
pkg_req: &NpmPackageReq,
|
||||
sub_path: Option<&str>,
|
||||
) -> String {
|
||||
let result = format!("{}{}", prefix, pkg_req);
|
||||
match sub_path {
|
||||
Some(path) => format!("{}/{}", result, path),
|
||||
None => result,
|
||||
}
|
||||
}
|
||||
|
||||
if self.npm_resolver.in_npm_package(specifier) {
|
||||
if let Ok(pkg_id) = self
|
||||
.npm_resolver
|
||||
.resolve_package_id_from_specifier(specifier)
|
||||
{
|
||||
// todo(dsherret): once supporting an import map, we should prioritize which
|
||||
// pkg requirement we use, based on what's specified in the import map
|
||||
if let Some(pkg_req) = self
|
||||
.npm_resolution
|
||||
.resolve_pkg_reqs_from_pkg_id(&pkg_id)
|
||||
.first()
|
||||
{
|
||||
let result = format!("npm:{}", pkg_req);
|
||||
return Some(match self.resolve_package_path(specifier) {
|
||||
Some(path) => format!("{}/{}", result, path),
|
||||
None => result,
|
||||
});
|
||||
let pkg_reqs =
|
||||
self.npm_resolution.resolve_pkg_reqs_from_pkg_id(&pkg_id);
|
||||
// check if any pkg reqs match what is found in an import map
|
||||
if !pkg_reqs.is_empty() {
|
||||
let sub_path = self.resolve_package_path(specifier);
|
||||
if let Some(import_map) = self.maybe_import_map {
|
||||
for pkg_req in &pkg_reqs {
|
||||
let paths = vec![
|
||||
concat_npm_specifier("npm:", pkg_req, sub_path.as_deref()),
|
||||
concat_npm_specifier("npm:/", pkg_req, sub_path.as_deref()),
|
||||
];
|
||||
for path in paths {
|
||||
if let Some(mapped_path) = ModuleSpecifier::parse(&path)
|
||||
.ok()
|
||||
.and_then(|s| import_map.lookup(&s, referrer))
|
||||
{
|
||||
return Some(mapped_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if not found in the import map, return the first pkg req
|
||||
if let Some(pkg_req) = pkg_reqs.first() {
|
||||
return Some(concat_npm_specifier(
|
||||
"npm:",
|
||||
pkg_req,
|
||||
sub_path.as_deref(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if the import map has this specifier
|
||||
if let Some(import_map) = self.maybe_import_map {
|
||||
if let Some(result) = import_map.lookup(specifier, referrer) {
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
|
@ -238,13 +285,13 @@ impl<'a> TsResponseImportMapper<'a> {
|
|||
/// Iterate over the supported extensions, concatenating the extension on the
|
||||
/// specifier, returning the first specifier that is resolve-able, otherwise
|
||||
/// None if none match.
|
||||
pub fn check_specifier_with_referrer(
|
||||
pub fn check_unresolved_specifier(
|
||||
&self,
|
||||
specifier: &str,
|
||||
referrer: &ModuleSpecifier,
|
||||
) -> Option<String> {
|
||||
if let Ok(specifier) = referrer.join(specifier) {
|
||||
if let Some(specifier) = self.check_specifier(&specifier) {
|
||||
if let Some(specifier) = self.check_specifier(&specifier, referrer) {
|
||||
return Some(specifier);
|
||||
}
|
||||
}
|
||||
|
@ -338,7 +385,7 @@ pub fn fix_ts_import_changes(
|
|||
if let Some(captures) = IMPORT_SPECIFIER_RE.captures(line) {
|
||||
let specifier = captures.get(1).unwrap().as_str();
|
||||
if let Some(new_specifier) =
|
||||
import_mapper.check_specifier_with_referrer(specifier, referrer)
|
||||
import_mapper.check_unresolved_specifier(specifier, referrer)
|
||||
{
|
||||
line.replace(specifier, &new_specifier)
|
||||
} else {
|
||||
|
@ -387,7 +434,7 @@ fn fix_ts_import_action(
|
|||
.ok_or_else(|| anyhow!("Missing capture."))?
|
||||
.as_str();
|
||||
if let Some(new_specifier) =
|
||||
import_mapper.check_specifier_with_referrer(specifier, referrer)
|
||||
import_mapper.check_unresolved_specifier(specifier, referrer)
|
||||
{
|
||||
let description = action.description.replace(specifier, &new_specifier);
|
||||
let changes = action
|
||||
|
|
|
@ -2085,6 +2085,7 @@ impl Inner {
|
|||
pub fn get_ts_response_import_mapper(&self) -> TsResponseImportMapper {
|
||||
TsResponseImportMapper::new(
|
||||
&self.documents,
|
||||
self.maybe_import_map.as_deref(),
|
||||
&self.npm.resolution,
|
||||
&self.npm.resolver,
|
||||
)
|
||||
|
|
|
@ -2532,7 +2532,9 @@ fn update_import_statement(
|
|||
if let Ok(import_specifier) = normalize_specifier(&import_data.file_name)
|
||||
{
|
||||
if let Some(new_module_specifier) = maybe_import_mapper
|
||||
.and_then(|m| m.check_specifier(&import_specifier))
|
||||
.and_then(|m| {
|
||||
m.check_specifier(&import_specifier, &item_data.specifier)
|
||||
})
|
||||
.or_else(|| {
|
||||
relative_specifier(&item_data.specifier, &import_specifier)
|
||||
})
|
||||
|
|
|
@ -4824,7 +4824,7 @@ fn lsp_completions_auto_import() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_npm_completions_auto_import_and_quick_fix() {
|
||||
fn lsp_npm_completions_auto_import_and_quick_fix_no_import_map() {
|
||||
let context = TestContextBuilder::new()
|
||||
.use_http_server()
|
||||
.use_temp_cwd()
|
||||
|
@ -5073,6 +5073,386 @@ fn lsp_npm_completions_auto_import_and_quick_fix() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_completions_auto_import_and_quick_fix_with_import_map() {
|
||||
let context = TestContextBuilder::new()
|
||||
.use_http_server()
|
||||
.use_temp_cwd()
|
||||
.build();
|
||||
let temp_dir = context.temp_dir();
|
||||
let import_map = r#"{
|
||||
"imports": {
|
||||
"print_hello": "http://localhost:4545/subdir/print_hello.ts",
|
||||
"chalk": "npm:chalk@~5",
|
||||
"types-exports-subpaths/": "npm:/@denotest/types-exports-subpaths@1/"
|
||||
}
|
||||
}"#;
|
||||
temp_dir.write("import_map.json", import_map);
|
||||
|
||||
let mut client = context.new_lsp_command().build();
|
||||
client.initialize(|builder| {
|
||||
builder.set_import_map("import_map.json");
|
||||
});
|
||||
client.did_open(
|
||||
json!({
|
||||
"textDocument": {
|
||||
"uri": "file:///a/file.ts",
|
||||
"languageId": "typescript",
|
||||
"version": 1,
|
||||
"text": concat!(
|
||||
"import {getClient} from 'npm:@denotest/types-exports-subpaths@1/client';\n",
|
||||
"import _test1 from 'npm:chalk@^5.0';\n",
|
||||
"import chalk from 'npm:chalk@~5';\n",
|
||||
"import chalk from 'npm:chalk@~5';\n",
|
||||
"import {printHello} from 'print_hello';\n",
|
||||
"\n",
|
||||
),
|
||||
}
|
||||
}),
|
||||
);
|
||||
client.write_request(
|
||||
"deno/cache",
|
||||
json!({
|
||||
"referrer": {
|
||||
"uri": "file:///a/file.ts",
|
||||
},
|
||||
"uris": [
|
||||
{
|
||||
"uri": "npm:@denotest/types-exports-subpaths@1/client",
|
||||
}, {
|
||||
"uri": "npm:chalk@^5.0",
|
||||
}, {
|
||||
"uri": "npm:chalk@~5",
|
||||
}, {
|
||||
"uri": "http://localhost:4545/subdir/print_hello.ts",
|
||||
}
|
||||
]
|
||||
}),
|
||||
);
|
||||
|
||||
// try auto-import with path
|
||||
client.did_open(json!({
|
||||
"textDocument": {
|
||||
"uri": "file:///a/a.ts",
|
||||
"languageId": "typescript",
|
||||
"version": 1,
|
||||
"text": "getClie",
|
||||
}
|
||||
}));
|
||||
let list = client.get_completion_list(
|
||||
"file:///a/a.ts",
|
||||
(0, 7),
|
||||
json!({ "triggerKind": 1 }),
|
||||
);
|
||||
assert!(!list.is_incomplete);
|
||||
let item = list
|
||||
.items
|
||||
.iter()
|
||||
.find(|item| item.label == "getClient")
|
||||
.unwrap();
|
||||
|
||||
let res = client.write_request("completionItem/resolve", item);
|
||||
assert_eq!(
|
||||
res,
|
||||
json!({
|
||||
"label": "getClient",
|
||||
"kind": 3,
|
||||
"detail": "function getClient(): 5",
|
||||
"documentation": {
|
||||
"kind": "markdown",
|
||||
"value": ""
|
||||
},
|
||||
"sortText": "16",
|
||||
"additionalTextEdits": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 0 }
|
||||
},
|
||||
"newText": "import { getClient } from \"types-exports-subpaths/client\";\n\n"
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
|
||||
// try quick fix with path
|
||||
let diagnostics = client.did_open(json!({
|
||||
"textDocument": {
|
||||
"uri": "file:///a/b.ts",
|
||||
"languageId": "typescript",
|
||||
"version": 1,
|
||||
"text": "getClient",
|
||||
}
|
||||
}));
|
||||
let diagnostics = diagnostics
|
||||
.messages_with_file_and_source("file:///a/b.ts", "deno-ts")
|
||||
.diagnostics;
|
||||
let res = client.write_request(
|
||||
"textDocument/codeAction",
|
||||
json!(json!({
|
||||
"textDocument": {
|
||||
"uri": "file:///a/b.ts"
|
||||
},
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 9 }
|
||||
},
|
||||
"context": {
|
||||
"diagnostics": diagnostics,
|
||||
"only": ["quickfix"]
|
||||
}
|
||||
})),
|
||||
);
|
||||
assert_eq!(
|
||||
res,
|
||||
json!([{
|
||||
"title": "Add import from \"types-exports-subpaths/client\"",
|
||||
"kind": "quickfix",
|
||||
"diagnostics": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 9 }
|
||||
},
|
||||
"severity": 1,
|
||||
"code": 2304,
|
||||
"source": "deno-ts",
|
||||
"message": "Cannot find name 'getClient'.",
|
||||
}
|
||||
],
|
||||
"edit": {
|
||||
"documentChanges": [{
|
||||
"textDocument": {
|
||||
"uri": "file:///a/b.ts",
|
||||
"version": 1,
|
||||
},
|
||||
"edits": [{
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 0 }
|
||||
},
|
||||
"newText": "import { getClient } from \"types-exports-subpaths/client\";\n\n"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}])
|
||||
);
|
||||
|
||||
// try auto-import without path
|
||||
client.did_open(json!({
|
||||
"textDocument": {
|
||||
"uri": "file:///a/c.ts",
|
||||
"languageId": "typescript",
|
||||
"version": 1,
|
||||
"text": "chal",
|
||||
}
|
||||
}));
|
||||
|
||||
let list = client.get_completion_list(
|
||||
"file:///a/c.ts",
|
||||
(0, 4),
|
||||
json!({ "triggerKind": 1 }),
|
||||
);
|
||||
assert!(!list.is_incomplete);
|
||||
let item = list
|
||||
.items
|
||||
.iter()
|
||||
.find(|item| item.label == "chalk")
|
||||
.unwrap();
|
||||
|
||||
let mut res = client.write_request("completionItem/resolve", item);
|
||||
let obj = res.as_object_mut().unwrap();
|
||||
obj.remove("detail"); // not worth testing these
|
||||
obj.remove("documentation");
|
||||
assert_eq!(
|
||||
res,
|
||||
json!({
|
||||
"label": "chalk",
|
||||
"kind": 6,
|
||||
"sortText": "16",
|
||||
"additionalTextEdits": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 0 }
|
||||
},
|
||||
"newText": "import chalk from \"chalk\";\n\n"
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
|
||||
// try quick fix without path
|
||||
let diagnostics = client.did_open(json!({
|
||||
"textDocument": {
|
||||
"uri": "file:///a/d.ts",
|
||||
"languageId": "typescript",
|
||||
"version": 1,
|
||||
"text": "chalk",
|
||||
}
|
||||
}));
|
||||
let diagnostics = diagnostics
|
||||
.messages_with_file_and_source("file:///a/d.ts", "deno-ts")
|
||||
.diagnostics;
|
||||
let res = client.write_request(
|
||||
"textDocument/codeAction",
|
||||
json!(json!({
|
||||
"textDocument": {
|
||||
"uri": "file:///a/d.ts"
|
||||
},
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 5 }
|
||||
},
|
||||
"context": {
|
||||
"diagnostics": diagnostics,
|
||||
"only": ["quickfix"]
|
||||
}
|
||||
})),
|
||||
);
|
||||
assert_eq!(
|
||||
res,
|
||||
json!([{
|
||||
"title": "Add import from \"chalk\"",
|
||||
"kind": "quickfix",
|
||||
"diagnostics": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 5 }
|
||||
},
|
||||
"severity": 1,
|
||||
"code": 2304,
|
||||
"source": "deno-ts",
|
||||
"message": "Cannot find name 'chalk'.",
|
||||
}
|
||||
],
|
||||
"edit": {
|
||||
"documentChanges": [{
|
||||
"textDocument": {
|
||||
"uri": "file:///a/d.ts",
|
||||
"version": 1,
|
||||
},
|
||||
"edits": [{
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 0 }
|
||||
},
|
||||
"newText": "import chalk from \"chalk\";\n\n"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}])
|
||||
);
|
||||
|
||||
// try auto-import with http import map
|
||||
client.did_open(json!({
|
||||
"textDocument": {
|
||||
"uri": "file:///a/e.ts",
|
||||
"languageId": "typescript",
|
||||
"version": 1,
|
||||
"text": "printH",
|
||||
}
|
||||
}));
|
||||
|
||||
let list = client.get_completion_list(
|
||||
"file:///a/e.ts",
|
||||
(0, 6),
|
||||
json!({ "triggerKind": 1 }),
|
||||
);
|
||||
assert!(!list.is_incomplete);
|
||||
let item = list
|
||||
.items
|
||||
.iter()
|
||||
.find(|item| item.label == "printHello")
|
||||
.unwrap();
|
||||
|
||||
let mut res = client.write_request("completionItem/resolve", item);
|
||||
let obj = res.as_object_mut().unwrap();
|
||||
obj.remove("detail"); // not worth testing these
|
||||
obj.remove("documentation");
|
||||
assert_eq!(
|
||||
res,
|
||||
json!({
|
||||
"label": "printHello",
|
||||
"kind": 3,
|
||||
"sortText": "16",
|
||||
"additionalTextEdits": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 0 }
|
||||
},
|
||||
"newText": "import { printHello } from \"print_hello\";\n\n"
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
|
||||
// try quick fix with http import
|
||||
let diagnostics = client.did_open(json!({
|
||||
"textDocument": {
|
||||
"uri": "file:///a/f.ts",
|
||||
"languageId": "typescript",
|
||||
"version": 1,
|
||||
"text": "printHello",
|
||||
}
|
||||
}));
|
||||
let diagnostics = diagnostics
|
||||
.messages_with_file_and_source("file:///a/f.ts", "deno-ts")
|
||||
.diagnostics;
|
||||
let res = client.write_request(
|
||||
"textDocument/codeAction",
|
||||
json!(json!({
|
||||
"textDocument": {
|
||||
"uri": "file:///a/f.ts"
|
||||
},
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 10 }
|
||||
},
|
||||
"context": {
|
||||
"diagnostics": diagnostics,
|
||||
"only": ["quickfix"]
|
||||
}
|
||||
})),
|
||||
);
|
||||
assert_eq!(
|
||||
res,
|
||||
json!([{
|
||||
"title": "Add import from \"print_hello\"",
|
||||
"kind": "quickfix",
|
||||
"diagnostics": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 10 }
|
||||
},
|
||||
"severity": 1,
|
||||
"code": 2304,
|
||||
"source": "deno-ts",
|
||||
"message": "Cannot find name 'printHello'.",
|
||||
}
|
||||
],
|
||||
"edit": {
|
||||
"documentChanges": [{
|
||||
"textDocument": {
|
||||
"uri": "file:///a/f.ts",
|
||||
"version": 1,
|
||||
},
|
||||
"edits": [{
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 0 }
|
||||
},
|
||||
"newText": "import { printHello } from \"print_hello\";\n\n"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_completions_snippet() {
|
||||
let context = TestContextBuilder::new().use_temp_cwd().build();
|
||||
|
|
Loading…
Add table
Reference in a new issue