mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 09:31:22 -05:00
feat(lsp): basic support of auto-imports for npm specifiers (#19675)
Closes #19625 Closes https://github.com/denoland/vscode_deno/issues/857
This commit is contained in:
parent
e746b6d806
commit
cfbc9b471f
11 changed files with 640 additions and 28 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
) -> 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);
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
None
|
||||
|
||||
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> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -4823,6 +4823,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();
|
||||
|
|
Loading…
Add table
Reference in a new issue