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:
David Sherret 2023-07-01 21:07:57 -04:00 committed by GitHub
parent e746b6d806
commit cfbc9b471f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 640 additions and 28 deletions

4
Cargo.lock generated
View file

@ -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",

View file

@ -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

View file

@ -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"
);
}
}

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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(

View file

@ -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

View file

@ -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);
}
}
}

View file

@ -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,

View file

@ -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();