summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock4
-rw-r--r--Cargo.toml2
-rw-r--r--cli/lsp/analysis.rs248
-rw-r--r--cli/lsp/language_server.rs11
-rw-r--r--cli/lsp/tsc.rs11
-rw-r--r--cli/npm/resolution.rs29
-rw-r--r--cli/npm/resolvers/common.rs6
-rw-r--r--cli/npm/resolvers/global.rs11
-rw-r--r--cli/npm/resolvers/local.rs79
-rw-r--r--cli/npm/resolvers/mod.rs15
-rw-r--r--cli/tests/integration/lsp_tests.rs250
11 files changed, 639 insertions, 27 deletions
diff --git a/Cargo.lock b/Cargo.lock
index b7fea19ce..01aea82a3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1314,9 +1314,9 @@ dependencies = [
[[package]]
name = "deno_npm"
-version = "0.9.0"
+version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4b0de941ffd64e68ec1adbaf24c045214be3232ca316f32f55b6b2197b4f5b3"
+checksum = "371ef0398b5b5460d66b78a958d5015658e198ad3a29fb9ce329459272fd29aa"
dependencies = [
"anyhow",
"async-trait",
diff --git a/Cargo.toml b/Cargo.toml
index 5cc401a95..e8060ca2a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -51,7 +51,7 @@ deno_bench_util = { version = "0.103.0", path = "./bench_util" }
test_util = { path = "./test_util" }
deno_lockfile = "0.14.1"
deno_media_type = { version = "0.1.0", features = ["module_specifier"] }
-deno_npm = "0.9.0"
+deno_npm = "0.9.1"
deno_semver = "0.2.2"
# exts
diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs
index a5ebbbbb8..80c748055 100644
--- a/cli/lsp/analysis.rs
+++ b/cli/lsp/analysis.rs
@@ -5,6 +5,8 @@ use super::documents::Documents;
use super::language_server;
use super::tsc;
+use crate::npm::CliNpmResolver;
+use crate::npm::NpmResolution;
use crate::tools::lint::create_linter;
use deno_ast::SourceRange;
@@ -14,13 +16,17 @@ use deno_core::anyhow::anyhow;
use deno_core::error::custom_error;
use deno_core::error::AnyError;
use deno_core::serde::Deserialize;
+use deno_core::serde_json;
use deno_core::serde_json::json;
use deno_core::ModuleSpecifier;
use deno_lint::rules::LintRule;
+use deno_runtime::deno_node::PackageJson;
+use deno_runtime::deno_node::PathClean;
use once_cell::sync::Lazy;
use regex::Regex;
use std::cmp::Ordering;
use std::collections::HashMap;
+use std::path::Path;
use tower_lsp::lsp_types as lsp;
use tower_lsp::lsp_types::Position;
use tower_lsp::lsp_types::Range;
@@ -148,21 +154,169 @@ fn code_as_string(code: &Option<lsp::NumberOrString>) -> String {
}
}
-/// Iterate over the supported extensions, concatenating the extension on the
-/// specifier, returning the first specifier that is resolve-able, otherwise
-/// None if none match.
-fn check_specifier(
- specifier: &str,
- referrer: &ModuleSpecifier,
- documents: &Documents,
+/// Rewrites imports in quick fixes and code changes to be Deno specific.
+pub struct TsResponseImportMapper<'a> {
+ documents: &'a Documents,
+ npm_resolution: &'a NpmResolution,
+ npm_resolver: &'a CliNpmResolver,
+}
+
+impl<'a> TsResponseImportMapper<'a> {
+ pub fn new(
+ documents: &'a Documents,
+ npm_resolution: &'a NpmResolution,
+ npm_resolver: &'a CliNpmResolver,
+ ) -> Self {
+ Self {
+ documents,
+ npm_resolution,
+ npm_resolver,
+ }
+ }
+
+ pub fn check_specifier(&self, specifier: &ModuleSpecifier) -> Option<String> {
+ 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,
+ });
+ }
+ }
+ }
+ None
+ }
+
+ fn resolve_package_path(
+ &self,
+ specifier: &ModuleSpecifier,
+ ) -> Option<String> {
+ let specifier_path = specifier.to_file_path().ok()?;
+ let root_folder = self
+ .npm_resolver
+ .resolve_package_folder_from_specifier(specifier)
+ .ok()?;
+ let package_json_path = root_folder.join("package.json");
+ let package_json_text = std::fs::read_to_string(&package_json_path).ok()?;
+ let package_json =
+ PackageJson::load_from_string(package_json_path, package_json_text)
+ .ok()?;
+
+ let mut search_paths = vec![specifier_path.clone()];
+ // TypeScript will provide a .js extension for quick fixes, so do
+ // a search for the .d.ts file instead
+ if specifier_path.extension().and_then(|e| e.to_str()) == Some("js") {
+ search_paths.insert(0, specifier_path.with_extension("d.ts"));
+ }
+
+ for search_path in search_paths {
+ if let Some(exports) = &package_json.exports {
+ if let Some(result) = try_reverse_map_package_json_exports(
+ &root_folder,
+ &search_path,
+ exports,
+ ) {
+ return Some(result);
+ }
+ }
+ }
+
+ None
+ }
+
+ /// 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(
+ &self,
+ specifier: &str,
+ referrer: &ModuleSpecifier,
+ ) -> Option<String> {
+ if let Ok(specifier) = referrer.join(specifier) {
+ if let Some(specifier) = self.check_specifier(&specifier) {
+ return Some(specifier);
+ }
+ }
+ for ext in SUPPORTED_EXTENSIONS {
+ let specifier_with_ext = format!("{specifier}{ext}");
+ if self
+ .documents
+ .contains_import(&specifier_with_ext, referrer)
+ {
+ return Some(specifier_with_ext);
+ }
+ }
+ None
+ }
+}
+
+fn try_reverse_map_package_json_exports(
+ root_path: &Path,
+ target_path: &Path,
+ exports: &serde_json::Map<String, serde_json::Value>,
) -> Option<String> {
- for ext in SUPPORTED_EXTENSIONS {
- let specifier_with_ext = format!("{specifier}{ext}");
- if documents.contains_import(&specifier_with_ext, referrer) {
- return Some(specifier_with_ext);
+ use deno_core::serde_json::Value;
+
+ fn try_reverse_map_package_json_exports_inner(
+ root_path: &Path,
+ target_path: &Path,
+ exports: &serde_json::Map<String, Value>,
+ ) -> Option<String> {
+ for (key, value) in exports {
+ match value {
+ Value::String(str) => {
+ if root_path.join(str).clean() == target_path {
+ return Some(if let Some(suffix) = key.strip_prefix("./") {
+ suffix.to_string()
+ } else {
+ String::new() // condition (ex. "types"), ignore
+ });
+ }
+ }
+ Value::Object(obj) => {
+ if let Some(result) = try_reverse_map_package_json_exports_inner(
+ root_path,
+ target_path,
+ obj,
+ ) {
+ return Some(if let Some(suffix) = key.strip_prefix("./") {
+ if result.is_empty() {
+ suffix.to_string()
+ } else {
+ format!("{}/{}", suffix, result)
+ }
+ } else {
+ result // condition (ex. "types"), ignore
+ });
+ }
+ }
+ _ => {}
+ }
}
+ None
+ }
+
+ let result = try_reverse_map_package_json_exports_inner(
+ root_path,
+ target_path,
+ exports,
+ )?;
+ if result.is_empty() {
+ None
+ } else {
+ Some(result)
}
- None
}
/// For a set of tsc changes, can them for any that contain something that looks
@@ -170,7 +324,7 @@ fn check_specifier(
pub fn fix_ts_import_changes(
referrer: &ModuleSpecifier,
changes: &[tsc::FileTextChanges],
- documents: &Documents,
+ import_mapper: &TsResponseImportMapper,
) -> Result<Vec<tsc::FileTextChanges>, AnyError> {
let mut r = Vec::new();
for change in changes {
@@ -184,7 +338,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) =
- check_specifier(specifier, referrer, documents)
+ import_mapper.check_specifier_with_referrer(specifier, referrer)
{
line.replace(specifier, &new_specifier)
} else {
@@ -215,7 +369,7 @@ pub fn fix_ts_import_changes(
fn fix_ts_import_action(
referrer: &ModuleSpecifier,
action: &tsc::CodeFixAction,
- documents: &Documents,
+ import_mapper: &TsResponseImportMapper,
) -> Result<tsc::CodeFixAction, AnyError> {
if action.fix_name == "import" {
let change = action
@@ -233,7 +387,7 @@ fn fix_ts_import_action(
.ok_or_else(|| anyhow!("Missing capture."))?
.as_str();
if let Some(new_specifier) =
- check_specifier(specifier, referrer, documents)
+ import_mapper.check_specifier_with_referrer(specifier, referrer)
{
let description = action.description.replace(specifier, &new_specifier);
let changes = action
@@ -554,8 +708,11 @@ impl CodeActionCollection {
"The action returned from TypeScript is unsupported.",
));
}
- let action =
- fix_ts_import_action(specifier, action, &language_server.documents)?;
+ let action = fix_ts_import_action(
+ specifier,
+ action,
+ &language_server.get_ts_response_import_mapper(),
+ )?;
let edit = ts_changes_to_edit(&action.changes, language_server)?;
let code_action = lsp::CodeAction {
title: action.description.clone(),
@@ -735,6 +892,8 @@ pub fn source_range_to_lsp_range(
#[cfg(test)]
mod tests {
+ use std::path::PathBuf;
+
use super::*;
#[test]
@@ -824,4 +983,57 @@ mod tests {
}
);
}
+
+ #[test]
+ fn test_try_reverse_map_package_json_exports() {
+ let exports = json!({
+ ".": {
+ "types": "./src/index.d.ts",
+ "browser": "./dist/module.js",
+ },
+ "./hooks": {
+ "types": "./hooks/index.d.ts",
+ "browser": "./dist/devtools.module.js",
+ },
+ "./utils": {
+ "types": {
+ "./sub_utils": "./utils_sub_utils.d.ts"
+ }
+ }
+ });
+ let exports = exports.as_object().unwrap();
+ assert_eq!(
+ try_reverse_map_package_json_exports(
+ &PathBuf::from("/project/"),
+ &PathBuf::from("/project/hooks/index.d.ts"),
+ exports,
+ )
+ .unwrap(),
+ "hooks"
+ );
+ assert_eq!(
+ try_reverse_map_package_json_exports(
+ &PathBuf::from("/project/"),
+ &PathBuf::from("/project/dist/devtools.module.js"),
+ exports,
+ )
+ .unwrap(),
+ "hooks"
+ );
+ assert!(try_reverse_map_package_json_exports(
+ &PathBuf::from("/project/"),
+ &PathBuf::from("/project/src/index.d.ts"),
+ exports,
+ )
+ .is_none());
+ assert_eq!(
+ try_reverse_map_package_json_exports(
+ &PathBuf::from("/project/"),
+ &PathBuf::from("/project/utils_sub_utils.d.ts"),
+ exports,
+ )
+ .unwrap(),
+ "utils/sub_utils"
+ );
+ }
}
diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs
index 95bdf8724..0b6051212 100644
--- a/cli/lsp/language_server.rs
+++ b/cli/lsp/language_server.rs
@@ -38,6 +38,7 @@ use super::analysis::fix_ts_import_changes;
use super::analysis::ts_changes_to_edit;
use super::analysis::CodeActionCollection;
use super::analysis::CodeActionData;
+use super::analysis::TsResponseImportMapper;
use super::cache;
use super::capabilities;
use super::client::Client;
@@ -2029,7 +2030,7 @@ impl Inner {
fix_ts_import_changes(
&code_action_data.specifier,
&combined_code_actions.changes,
- &self.documents,
+ &self.get_ts_response_import_mapper(),
)
.map_err(|err| {
error!("Unable to remap changes: {}", err);
@@ -2081,6 +2082,14 @@ impl Inner {
Ok(result)
}
+ pub fn get_ts_response_import_mapper(&self) -> TsResponseImportMapper {
+ TsResponseImportMapper::new(
+ &self.documents,
+ &self.npm.resolution,
+ &self.npm.resolver,
+ )
+ }
+
async fn code_lens(
&self,
params: CodeLensParams,
diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs
index 20edf31d9..00a5d2bc7 100644
--- a/cli/lsp/tsc.rs
+++ b/cli/lsp/tsc.rs
@@ -1,6 +1,7 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
use super::analysis::CodeActionData;
+use super::analysis::TsResponseImportMapper;
use super::code_lens;
use super::config;
use super::documents::AssetOrDocument;
@@ -2326,6 +2327,7 @@ fn parse_code_actions(
update_import_statement(
tc.as_text_edit(asset_or_doc.line_index()),
data,
+ Some(&language_server.get_ts_response_import_mapper()),
)
}));
} else {
@@ -2521,6 +2523,7 @@ struct CompletionEntryDataImport {
fn update_import_statement(
mut text_edit: lsp::TextEdit,
item_data: &CompletionItemData,
+ maybe_import_mapper: Option<&TsResponseImportMapper>,
) -> lsp::TextEdit {
if let Some(data) = &item_data.data {
if let Ok(import_data) =
@@ -2528,8 +2531,11 @@ fn update_import_statement(
{
if let Ok(import_specifier) = normalize_specifier(&import_data.file_name)
{
- if let Some(new_module_specifier) =
- relative_specifier(&item_data.specifier, &import_specifier)
+ if let Some(new_module_specifier) = maybe_import_mapper
+ .and_then(|m| m.check_specifier(&import_specifier))
+ .or_else(|| {
+ relative_specifier(&item_data.specifier, &import_specifier)
+ })
{
text_edit.new_text = text_edit
.new_text
@@ -4716,6 +4722,7 @@ mod tests {
new_text: orig_text.to_string(),
},
&item_data,
+ None,
);
assert_eq!(
actual,
diff --git a/cli/npm/resolution.rs b/cli/npm/resolution.rs
index 37c8aa08c..95d9fd4c9 100644
--- a/cli/npm/resolution.rs
+++ b/cli/npm/resolution.rs
@@ -16,6 +16,7 @@ use deno_npm::resolution::NpmResolutionError;
use deno_npm::resolution::NpmResolutionSnapshot;
use deno_npm::resolution::NpmResolutionSnapshotPendingResolver;
use deno_npm::resolution::NpmResolutionSnapshotPendingResolverOptions;
+use deno_npm::resolution::PackageCacheFolderIdNotFoundError;
use deno_npm::resolution::PackageNotFoundFromReferrerError;
use deno_npm::resolution::PackageNvNotFoundError;
use deno_npm::resolution::PackageReqNotFoundError;
@@ -145,7 +146,7 @@ impl NpmResolution {
Ok(())
}
- pub fn resolve_package_cache_folder_id_from_id(
+ pub fn resolve_pkg_cache_folder_id_from_pkg_id(
&self,
id: &NpmPackageId,
) -> Option<NpmPackageCacheFolderId> {
@@ -156,6 +157,17 @@ impl NpmResolution {
.map(|p| p.get_package_cache_folder_id())
}
+ pub fn resolve_pkg_id_from_pkg_cache_folder_id(
+ &self,
+ id: &NpmPackageCacheFolderId,
+ ) -> Result<NpmPackageId, PackageCacheFolderIdNotFoundError> {
+ self
+ .snapshot
+ .read()
+ .resolve_pkg_from_pkg_cache_folder_id(id)
+ .map(|pkg| pkg.id.clone())
+ }
+
pub fn resolve_package_from_package(
&self,
name: &str,
@@ -180,6 +192,21 @@ impl NpmResolution {
.map(|pkg| pkg.id.clone())
}
+ pub fn resolve_pkg_reqs_from_pkg_id(
+ &self,
+ id: &NpmPackageId,
+ ) -> Vec<NpmPackageReq> {
+ let snapshot = self.snapshot.read();
+ let mut pkg_reqs = snapshot
+ .package_reqs()
+ .iter()
+ .filter(|(_, nv)| *nv == &id.nv)
+ .map(|(req, _)| req.clone())
+ .collect::<Vec<_>>();
+ pkg_reqs.sort(); // be deterministic
+ pkg_reqs
+ }
+
pub fn resolve_pkg_id_from_deno_module(
&self,
id: &NpmPackageNv,
diff --git a/cli/npm/resolvers/common.rs b/cli/npm/resolvers/common.rs
index e0705f12b..e8acc5c3a 100644
--- a/cli/npm/resolvers/common.rs
+++ b/cli/npm/resolvers/common.rs
@@ -13,6 +13,7 @@ use deno_core::error::AnyError;
use deno_core::futures;
use deno_core::task::spawn;
use deno_core::url::Url;
+use deno_npm::NpmPackageCacheFolderId;
use deno_npm::NpmPackageId;
use deno_npm::NpmResolutionPackage;
use deno_runtime::deno_fs::FileSystem;
@@ -47,6 +48,11 @@ pub trait NpmPackageFsResolver: Send + Sync {
specifier: &ModuleSpecifier,
) -> Result<PathBuf, AnyError>;
+ fn resolve_package_cache_folder_id_from_specifier(
+ &self,
+ specifier: &ModuleSpecifier,
+ ) -> Result<NpmPackageCacheFolderId, AnyError>;
+
async fn cache_packages(&self) -> Result<(), AnyError>;
fn ensure_read_permission(
diff --git a/cli/npm/resolvers/global.rs b/cli/npm/resolvers/global.rs
index ca84d7e8b..d1962ff04 100644
--- a/cli/npm/resolvers/global.rs
+++ b/cli/npm/resolvers/global.rs
@@ -82,7 +82,7 @@ impl NpmPackageFsResolver for GlobalNpmPackageResolver {
fn package_folder(&self, id: &NpmPackageId) -> Result<PathBuf, AnyError> {
let folder_id = self
.resolution
- .resolve_package_cache_folder_id_from_id(id)
+ .resolve_pkg_cache_folder_id_from_pkg_id(id)
.unwrap();
Ok(
self
@@ -131,6 +131,15 @@ impl NpmPackageFsResolver for GlobalNpmPackageResolver {
)
}
+ fn resolve_package_cache_folder_id_from_specifier(
+ &self,
+ specifier: &ModuleSpecifier,
+ ) -> Result<NpmPackageCacheFolderId, AnyError> {
+ self
+ .cache
+ .resolve_package_folder_id_from_specifier(specifier, &self.registry_url)
+ }
+
async fn cache_packages(&self) -> Result<(), AnyError> {
let package_partitions = self
.resolution
diff --git a/cli/npm/resolvers/local.rs b/cli/npm/resolvers/local.rs
index 976038404..670f41480 100644
--- a/cli/npm/resolvers/local.rs
+++ b/cli/npm/resolvers/local.rs
@@ -11,6 +11,7 @@ use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
+use crate::npm::cache::mixed_case_package_name_decode;
use crate::util::fs::symlink_dir;
use crate::util::fs::LaxSingleProcessFsFlag;
use crate::util::progress_bar::ProgressBar;
@@ -33,6 +34,7 @@ use deno_runtime::deno_fs;
use deno_runtime::deno_node::NodePermissions;
use deno_runtime::deno_node::NodeResolutionMode;
use deno_runtime::deno_node::PackageJson;
+use deno_semver::npm::NpmPackageNv;
use crate::npm::cache::mixed_case_package_name_encode;
use crate::npm::cache::should_sync_download;
@@ -137,7 +139,7 @@ impl NpmPackageFsResolver for LocalNpmPackageResolver {
}
fn package_folder(&self, id: &NpmPackageId) -> Result<PathBuf, AnyError> {
- match self.resolution.resolve_package_cache_folder_id_from_id(id) {
+ match self.resolution.resolve_pkg_cache_folder_id_from_pkg_id(id) {
// package is stored at:
// node_modules/.deno/<package_cache_folder_id_folder_name>/node_modules/<package_name>
Some(cache_folder_id) => Ok(
@@ -215,6 +217,18 @@ impl NpmPackageFsResolver for LocalNpmPackageResolver {
Ok(package_root_path)
}
+ fn resolve_package_cache_folder_id_from_specifier(
+ &self,
+ specifier: &ModuleSpecifier,
+ ) -> Result<NpmPackageCacheFolderId, AnyError> {
+ let folder_path = self.resolve_package_folder_from_specifier(specifier)?;
+ let folder_name = folder_path.parent().unwrap().to_string_lossy();
+ match get_package_folder_id_from_folder_name(&folder_name) {
+ Some(package_folder_id) => Ok(package_folder_id),
+ None => bail!("could not resolve package from specifier '{}'", specifier),
+ }
+ }
+
async fn cache_packages(&self) -> Result<(), AnyError> {
sync_resolution_with_fs(
&self.resolution.snapshot(),
@@ -471,6 +485,30 @@ fn get_package_folder_id_folder_name(
format!("{}@{}{}", name, nv.version, copy_str).replace('/', "+")
}
+fn get_package_folder_id_from_folder_name(
+ folder_name: &str,
+) -> Option<NpmPackageCacheFolderId> {
+ let folder_name = folder_name.replace('+', "/");
+ let (name, ending) = folder_name.rsplit_once('@')?;
+ let name = if let Some(encoded_name) = name.strip_prefix('_') {
+ mixed_case_package_name_decode(encoded_name)?
+ } else {
+ name.to_string()
+ };
+ let (raw_version, copy_index) = match ending.split_once('_') {
+ Some((raw_version, copy_index)) => {
+ let copy_index = copy_index.parse::<u8>().ok()?;
+ (raw_version, copy_index)
+ }
+ None => (ending, 0),
+ };
+ let version = deno_semver::Version::parse_from_npm(raw_version).ok()?;
+ Some(NpmPackageCacheFolderId {
+ nv: NpmPackageNv { name, version },
+ copy_index,
+ })
+}
+
fn symlink_package_dir(
old_path: &Path,
new_path: &Path,
@@ -531,3 +569,42 @@ fn join_package_name(path: &Path, package_name: &str) -> PathBuf {
}
path
}
+
+#[cfg(test)]
+mod test {
+ use deno_npm::NpmPackageCacheFolderId;
+ use deno_semver::npm::NpmPackageNv;
+
+ use super::*;
+
+ #[test]
+ fn test_get_package_folder_id_folder_name() {
+ let cases = vec![
+ (
+ NpmPackageCacheFolderId {
+ nv: NpmPackageNv {
+ name: "@types/foo".to_string(),
+ version: deno_semver::Version::parse_standard("1.2.3").unwrap(),
+ },
+ copy_index: 1,
+ },
+ "@types+foo@1.2.3_1".to_string(),
+ ),
+ (
+ NpmPackageCacheFolderId {
+ nv: NpmPackageNv {
+ name: "JSON".to_string(),
+ version: deno_semver::Version::parse_standard("3.2.1").unwrap(),
+ },
+ copy_index: 0,
+ },
+ "_jjju6tq@3.2.1".to_string(),
+ ),
+ ];
+ for (input, output) in cases {
+ assert_eq!(get_package_folder_id_folder_name(&input), output);
+ let folder_id = get_package_folder_id_from_folder_name(&output).unwrap();
+ assert_eq!(folder_id, input);
+ }
+ }
+}
diff --git a/cli/npm/resolvers/mod.rs b/cli/npm/resolvers/mod.rs
index abfe668c3..39fcba3fc 100644
--- a/cli/npm/resolvers/mod.rs
+++ b/cli/npm/resolvers/mod.rs
@@ -143,6 +143,21 @@ impl CliNpmResolver {
Ok(path)
}
+ /// Resolves the package nv from the provided specifier.
+ pub fn resolve_package_id_from_specifier(
+ &self,
+ specifier: &ModuleSpecifier,
+ ) -> Result<NpmPackageId, AnyError> {
+ let cache_folder_id = self
+ .fs_resolver
+ .resolve_package_cache_folder_id_from_specifier(specifier)?;
+ Ok(
+ self
+ .resolution
+ .resolve_pkg_id_from_pkg_cache_folder_id(&cache_folder_id)?,
+ )
+ }
+
/// Attempts to get the package size in bytes.
pub fn package_size(
&self,
diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs
index 52f1e55ba..f47fb7c82 100644
--- a/cli/tests/integration/lsp_tests.rs
+++ b/cli/tests/integration/lsp_tests.rs
@@ -4824,6 +4824,256 @@ fn lsp_completions_auto_import() {
}
#[test]
+fn lsp_npm_completions_auto_import_and_quick_fix() {
+ let context = TestContextBuilder::new()
+ .use_http_server()
+ .use_temp_cwd()
+ .build();
+ let mut client = context.new_lsp_command().build();
+ client.initialize_default();
+ client.did_open(
+ json!({
+ "textDocument": {
+ "uri": "file:///a/file.ts",
+ "languageId": "typescript",
+ "version": 1,
+ "text": "import {getClient} from 'npm:@denotest/types-exports-subpaths@1/client';import chalk from 'npm:chalk@5.0';\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",
+ }
+ ]
+ }),
+ );
+
+ // 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 \"npm:@denotest/types-exports-subpaths@1/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 \"npm:@denotest/types-exports-subpaths@1/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 \"npm:@denotest/types-exports-subpaths@1/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 \"npm:chalk@5.0\";\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 \"npm:chalk@5.0\"",
+ "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 \"npm:chalk@5.0\";\n\n"
+ }]
+ }]
+ }
+ }])
+ );
+}
+
+#[test]
fn lsp_completions_snippet() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let mut client = context.new_lsp_command().build();