diff --git a/Cargo.lock b/Cargo.lock index b7fea19cec..01aea82a3d 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 5cc401a95d..e8060ca2a1 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 a5ebbbbb81..80c7480559 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) -> 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 { - 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 { + 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 { + 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 { + 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, +) -> Option { + use deno_core::serde_json::Value; + + fn try_reverse_map_package_json_exports_inner( + root_path: &Path, + target_path: &Path, + exports: &serde_json::Map, + ) -> Option { + 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, 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 { 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 95bdf87240..0b6051212c 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 20edf31d96..00a5d2bc74 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 37c8aa08c2..95d9fd4c9d 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 { @@ -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 { + 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 { + let snapshot = self.snapshot.read(); + let mut pkg_reqs = snapshot + .package_reqs() + .iter() + .filter(|(_, nv)| *nv == &id.nv) + .map(|(req, _)| req.clone()) + .collect::>(); + 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 e0705f12bb..e8acc5c3ab 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; + fn resolve_package_cache_folder_id_from_specifier( + &self, + specifier: &ModuleSpecifier, + ) -> Result; + 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 ca84d7e8bc..d1962ff042 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 { 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 { + 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 976038404d..670f414809 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 { - 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//node_modules/ 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 { + 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 { + 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::().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 abfe668c39..39fcba3fc9 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 { + 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 52f1e55ba1..f47fb7c823 100644 --- a/cli/tests/integration/lsp_tests.rs +++ b/cli/tests/integration/lsp_tests.rs @@ -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();