summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKitson Kelly <me@kitsonkelly.com>2022-02-10 07:13:50 +1100
committerGitHub <noreply@github.com>2022-02-10 07:13:50 +1100
commit773f882e5e7bcb93d4fd3ab66e56c6e422dfc97a (patch)
treea84c33fe3a666ab90f7b4c3e9c722e5533be1b90
parente218d567d5af0f778541c4b81f171d4fb1427db1 (diff)
feat(lsp): provide completions from import map if available (#13624)
Closes #13619
-rw-r--r--Cargo.lock4
-rw-r--r--cli/Cargo.toml2
-rw-r--r--cli/lsp/completions.rs151
-rw-r--r--cli/lsp/language_server.rs1
-rw-r--r--cli/proc_state.rs3
-rw-r--r--cli/tests/integration/lsp_tests.rs190
-rw-r--r--cli/tests/testdata/lsp/import-map-completions.json7
7 files changed, 351 insertions, 7 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 695e9ba7c..49d478a00 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index 2001a1d44..1943a635f 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -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"] }
diff --git a/cli/lsp/completions.rs b/cli/lsp/completions.rs
index c3026697a..5a09ada61 100644
--- a/cli/lsp/completions.rs
+++ b/cli/lsp/completions.rs
@@ -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,
diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs
index bf7be3ea7..e561e5d4e 100644
--- a/cli/lsp/language_server.rs
+++ b/cli/lsp/language_server.rs
@@ -1649,6 +1649,7 @@ impl Inner {
self.client.clone(),
&self.module_registries,
&self.documents,
+ self.maybe_import_map.clone(),
)
.await
{
diff --git a/cli/proc_state.rs b/cli/proc_state.rs
index 320e20ac4..5b347d169 100644
--- a/cli/proc_state.rs
+++ b/cli/proc_state.rs
@@ -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{}",
diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs
index 7607582c8..d45b90955 100644
--- a/cli/tests/integration/lsp_tests.rs
+++ b/cli/tests/integration/lsp_tests.rs
@@ -548,6 +548,196 @@ fn lsp_import_assertions() {
}
#[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");
did_open(
diff --git a/cli/tests/testdata/lsp/import-map-completions.json b/cli/tests/testdata/lsp/import-map-completions.json
new file mode 100644
index 000000000..f2275222a
--- /dev/null
+++ b/cli/tests/testdata/lsp/import-map-completions.json
@@ -0,0 +1,7 @@
+{
+ "imports": {
+ "/~/": "./lib/",
+ "fs": "https://example.com/fs/index.js",
+ "std/": "https://example.com/std@0.123.0/"
+ }
+}