1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-26 17:19:06 -05:00

feat(lsp): provide completions from import map if available (#13624)

Closes #13619
This commit is contained in:
Kitson Kelly 2022-02-10 07:13:50 +11:00 committed by GitHub
parent e218d567d5
commit 773f882e5e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 351 additions and 7 deletions

4
Cargo.lock generated
View file

@ -1935,9 +1935,9 @@ checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
[[package]]
name = "import_map"
version = "0.6.0"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f64f821df8ee00a0fba2dde6296af519eff7d823542b057c1b8c40ca1d58f4c"
checksum = "09ae88504e9128c4c181a0a4726d868d52aa76de270c7fb00c3c40a8f4fbace4"
dependencies = [
"indexmap",
"log",

View file

@ -68,7 +68,7 @@ encoding_rs = "=0.8.29"
env_logger = "=0.8.4"
fancy-regex = "=0.7.1"
http = "=0.2.4"
import_map = "=0.6.0"
import_map = "=0.8.0"
jsonc-parser = { version = "=0.19.0", features = ["serde"] }
libc = "=0.2.106"
log = { version = "=0.4.14", features = ["serde"] }

View file

@ -21,7 +21,14 @@ use deno_core::serde::Deserialize;
use deno_core::serde::Serialize;
use deno_core::url::Position;
use deno_core::ModuleSpecifier;
use import_map::ImportMap;
use lspower::lsp;
use once_cell::sync::Lazy;
use regex::Regex;
use std::sync::Arc;
static FILE_PROTO_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"^file:/{2}(?:/[A-Za-z]:)?"#).unwrap());
const CURRENT_PATH: &str = ".";
const PARENT_PATH: &str = "..";
@ -126,12 +133,22 @@ pub(crate) async fn get_import_completions(
client: Client,
module_registries: &ModuleRegistry,
documents: &Documents,
maybe_import_map: Option<Arc<ImportMap>>,
) -> Option<lsp::CompletionResponse> {
let document = documents.get(specifier)?;
let (text, _, range) = document.get_maybe_dependency(position)?;
let range = to_narrow_lsp_range(&document.text_info(), &range);
// completions for local relative modules
if text.starts_with("./") || text.starts_with("../") {
if let Some(completion_list) = get_import_map_completions(
specifier,
&text,
&range,
maybe_import_map.clone(),
documents,
) {
// completions for import map specifiers
Some(lsp::CompletionResponse::List(completion_list))
} else if text.starts_with("./") || text.starts_with("../") {
// completions for local relative modules
Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: false,
items: get_local_completions(specifier, &text, &range)?,
@ -155,6 +172,8 @@ pub(crate) async fn get_import_completions(
});
Some(lsp::CompletionResponse::List(list))
} else {
// the import specifier is empty, so provide all possible specifiers we are
// aware of
let mut items: Vec<lsp::CompletionItem> = LOCAL_PATHS
.iter()
.map(|s| lsp::CompletionItem {
@ -167,6 +186,9 @@ pub(crate) async fn get_import_completions(
})
.collect();
let mut is_incomplete = false;
if let Some(import_map) = maybe_import_map {
items.extend(get_base_import_map_completions(import_map.as_ref()));
}
if let Some(origin_items) =
module_registries.get_origin_completions(&text, &range)
{
@ -177,10 +199,133 @@ pub(crate) async fn get_import_completions(
is_incomplete,
items,
}))
// TODO(@kitsonk) add bare specifiers from import map
}
}
/// When the specifier is an empty string, return all the keys from the import
/// map as completion items.
fn get_base_import_map_completions(
import_map: &ImportMap,
) -> Vec<lsp::CompletionItem> {
import_map
.imports_keys()
.iter()
.map(|key| {
// for some strange reason, keys that start with `/` get stored in the
// import map as `file:///`, and so when we pull the keys out, we need to
// change the behavior
let mut label = if key.starts_with("file://") {
FILE_PROTO_RE.replace(key, "").to_string()
} else {
key.to_string()
};
let kind = if key.ends_with('/') {
label.pop();
Some(lsp::CompletionItemKind::FOLDER)
} else {
Some(lsp::CompletionItemKind::FILE)
};
lsp::CompletionItem {
label: label.clone(),
kind,
detail: Some("(import map)".to_string()),
sort_text: Some(label.clone()),
insert_text: Some(label),
..Default::default()
}
})
.collect()
}
/// Given an existing specifier, return any completions that could apply derived
/// from the import map. There are two main type of import map keys, those that
/// a literal, which don't end in `/`, which expects a one for one replacement
/// of specifier to specifier, and then those that end in `/` which indicates
/// that the path post the `/` should be appended to resolved specifier. This
/// handles both cases, pulling any completions from the workspace completions.
fn get_import_map_completions(
specifier: &ModuleSpecifier,
text: &str,
range: &lsp::Range,
maybe_import_map: Option<Arc<ImportMap>>,
documents: &Documents,
) -> Option<lsp::CompletionList> {
if !text.is_empty() {
if let Some(import_map) = maybe_import_map {
let mut items = Vec::new();
for key in import_map.imports_keys() {
// for some reason, the import_map stores keys that begin with `/` as
// `file:///` in its index, so we have to reverse that here
let key = if key.starts_with("file://") {
FILE_PROTO_RE.replace(key, "").to_string()
} else {
key.to_string()
};
if text.starts_with(&key) && key.ends_with('/') {
if let Ok(resolved) = import_map.resolve(&key, specifier) {
let resolved = resolved.to_string();
let workspace_items: Vec<lsp::CompletionItem> = documents
.documents(false, true)
.into_iter()
.filter_map(|d| {
let specifier_str = d.specifier().to_string();
let new_text = specifier_str.replace(&resolved, &key);
if specifier_str.starts_with(&resolved) {
let label = specifier_str.replace(&resolved, "");
let text_edit =
Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: *range,
new_text: new_text.clone(),
}));
Some(lsp::CompletionItem {
label,
kind: Some(lsp::CompletionItemKind::MODULE),
detail: Some("(import map)".to_string()),
sort_text: Some("1".to_string()),
filter_text: Some(new_text),
text_edit,
..Default::default()
})
} else {
None
}
})
.collect();
items.extend(workspace_items);
}
} else if key.starts_with(text) && text != key {
let mut label = key.to_string();
let kind = if key.ends_with('/') {
label.pop();
Some(lsp::CompletionItemKind::FOLDER)
} else {
Some(lsp::CompletionItemKind::MODULE)
};
let text_edit = Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: *range,
new_text: label.clone(),
}));
items.push(lsp::CompletionItem {
label: label.clone(),
kind,
detail: Some("(import map)".to_string()),
sort_text: Some("1".to_string()),
text_edit,
..Default::default()
});
}
if !items.is_empty() {
return Some(lsp::CompletionList {
items,
is_incomplete: false,
});
}
}
}
}
None
}
/// Return local completions that are relative to the base specifier.
fn get_local_completions(
base: &ModuleSpecifier,

View file

@ -1649,6 +1649,7 @@ impl Inner {
self.client.clone(),
&self.module_registries,
&self.documents,
self.maybe_import_map.clone(),
)
.await
{

View file

@ -49,6 +49,7 @@ use deno_runtime::deno_tls::rustls::RootCertStore;
use deno_runtime::deno_web::BlobStore;
use deno_runtime::inspector_server::InspectorServer;
use deno_runtime::permissions::Permissions;
use import_map::parse_from_json;
use import_map::ImportMap;
use log::warn;
use std::collections::HashSet;
@ -617,7 +618,7 @@ pub fn import_map_from_text(
specifier: &Url,
json_text: &str,
) -> Result<ImportMap, AnyError> {
let result = ImportMap::from_json_with_diagnostics(specifier, json_text)?;
let result = parse_from_json(specifier, json_text)?;
if !result.diagnostics.is_empty() {
warn!(
"Import map diagnostics:\n{}",

View file

@ -547,6 +547,196 @@ fn lsp_import_assertions() {
shutdown(&mut client);
}
#[test]
fn lsp_import_map_import_completions() {
let temp_dir = TempDir::new().expect("could not create temp dir");
let mut params: lsp::InitializeParams =
serde_json::from_value(load_fixture("initialize_params.json")).unwrap();
let import_map =
serde_json::to_vec_pretty(&load_fixture("import-map-completions.json"))
.unwrap();
fs::write(temp_dir.path().join("import-map.json"), import_map).unwrap();
fs::create_dir(temp_dir.path().join("lib")).unwrap();
fs::write(
temp_dir.path().join("lib").join("b.ts"),
r#"export const b = "b";"#,
)
.unwrap();
params.root_uri = Some(Url::from_file_path(temp_dir.path()).unwrap());
if let Some(Value::Object(mut map)) = params.initialization_options {
map.insert("importMap".to_string(), json!("import-map.json"));
params.initialization_options = Some(Value::Object(map));
}
let deno_exe = deno_exe_path();
let mut client = LspClient::new(&deno_exe).unwrap();
client
.write_request::<_, _, Value>("initialize", params)
.unwrap();
client.write_notification("initialized", json!({})).unwrap();
let uri = Url::from_file_path(temp_dir.path().join("a.ts")).unwrap();
did_open(
&mut client,
json!({
"textDocument": {
"uri": uri,
"languageId": "typescript",
"version": 1,
"text": "import * as a from \"/~/b.ts\";\nimport * as b from \"\""
}
}),
);
let (maybe_res, maybe_err) = client
.write_request(
"textDocument/completion",
json!({
"textDocument": {
"uri": uri
},
"position": {
"line": 1,
"character": 20
},
"context": {
"triggerKind": 2,
"triggerCharacter": "\""
}
}),
)
.unwrap();
assert!(maybe_err.is_none());
assert_eq!(
maybe_res,
Some(json!({
"isIncomplete": false,
"items": [
{
"label": ".",
"kind": 19,
"detail": "(local)",
"sortText": "1",
"insertText": "."
},
{
"label": "..",
"kind": 19,
"detail": "(local)",
"sortText": "1",
"insertText": ".."
},
{
"label": "std",
"kind": 19,
"detail": "(import map)",
"sortText": "std",
"insertText": "std"
},
{
"label": "fs",
"kind": 17,
"detail": "(import map)",
"sortText": "fs",
"insertText": "fs"
},
{
"label": "/~",
"kind": 19,
"detail": "(import map)",
"sortText": "/~",
"insertText": "/~"
}
]
}))
);
client
.write_notification(
"textDocument/didChange",
json!({
"textDocument": {
"uri": uri,
"version": 2
},
"contentChanges": [
{
"range": {
"start": {
"line": 1,
"character": 20
},
"end": {
"line": 1,
"character": 20
}
},
"text": "/~/"
}
]
}),
)
.unwrap();
let (method, _) = client.read_notification::<Value>().unwrap();
assert_eq!(method, "textDocument/publishDiagnostics");
let (method, _) = client.read_notification::<Value>().unwrap();
assert_eq!(method, "textDocument/publishDiagnostics");
let (method, _) = client.read_notification::<Value>().unwrap();
assert_eq!(method, "textDocument/publishDiagnostics");
let (maybe_res, maybe_err) = client
.write_request(
"textDocument/completion",
json!({
"textDocument": {
"uri": uri
},
"position": {
"line": 1,
"character": 23
},
"context": {
"triggerKind": 2,
"triggerCharacter": "/"
}
}),
)
.unwrap();
assert!(maybe_err.is_none());
assert_eq!(
maybe_res,
Some(json!({
"isIncomplete": false,
"items": [
{
"label": "b.ts",
"kind": 9,
"detail": "(import map)",
"sortText": "1",
"filterText": "/~/b.ts",
"textEdit": {
"range": {
"start": {
"line": 1,
"character": 20
},
"end": {
"line": 1,
"character": 23
}
},
"newText": "/~/b.ts"
}
}
]
}))
);
shutdown(&mut client);
}
#[test]
fn lsp_hover() {
let mut client = init("initialize_params.json");

View file

@ -0,0 +1,7 @@
{
"imports": {
"/~/": "./lib/",
"fs": "https://example.com/fs/index.js",
"std/": "https://example.com/std@0.123.0/"
}
}