From 3659781f88236a369aa9ca5142c0fb7d690fc898 Mon Sep 17 00:00:00 2001 From: Nayeem Rahman Date: Tue, 30 Jul 2024 23:26:09 +0100 Subject: [PATCH] feat(lsp): registry completions for import-mapped specifiers (#24792) --- cli/lsp/completions.rs | 9 +- cli/lsp/registries.rs | 800 +++++++++++++++++++++-------------------- 2 files changed, 420 insertions(+), 389 deletions(-) diff --git a/cli/lsp/completions.rs b/cli/lsp/completions.rs index 17fb0f719c..a51edcb332 100644 --- a/cli/lsp/completions.rs +++ b/cli/lsp/completions.rs @@ -215,16 +215,13 @@ pub async fn get_import_completions( module_registries, ) .await; - let offset = if position.character > range.start.character { - (position.character - range.start.character) as usize - } else { - 0 - }; let maybe_list = module_registries - .get_completions(&text, offset, &range, |s| { + .get_completions(&text, &range, resolved.as_ref(), |s| { documents.exists(s, file_referrer) }) .await; + let maybe_list = maybe_list + .or_else(|| module_registries.get_origin_completions(&text, &range)); let list = maybe_list.unwrap_or_else(|| CompletionList { items: get_workspace_completions(specifier, &text, &range, documents), is_incomplete: false, diff --git a/cli/lsp/registries.rs b/cli/lsp/registries.rs index e194c61107..c709831d42 100644 --- a/cli/lsp/registries.rs +++ b/cli/lsp/registries.rs @@ -33,6 +33,7 @@ use deno_graph::Dependency; use deno_runtime::deno_permissions::PermissionsContainer; use log::error; use once_cell::sync::Lazy; +use std::borrow::Cow; use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; @@ -86,23 +87,23 @@ enum CompletionType { /// Determine if a completion at a given offset is a string literal or a key/ /// variable. fn get_completion_type( - offset: usize, + char_offset: usize, tokens: &[Token], match_result: &MatchResult, ) -> Option { - let mut len = 0_usize; + let mut char_count = 0_usize; for (index, token) in tokens.iter().enumerate() { match token { Token::String(s) => { - len += s.chars().count(); - if offset < len { + char_count += s.chars().count(); + if char_offset < char_count { return Some(CompletionType::Literal(s.clone())); } } Token::Key(k) => { if let Some(prefix) = &k.prefix { - len += prefix.chars().count(); - if offset < len { + char_count += prefix.chars().count(); + if char_offset < char_count { return Some(CompletionType::Key { key: k.clone(), prefix: Some(prefix.clone()), @@ -110,7 +111,7 @@ fn get_completion_type( }); } } - if offset < len { + if char_offset < char_count { return None; } if let StringOrNumber::String(name) = &k.name { @@ -118,8 +119,8 @@ fn get_completion_type( .get(name) .map(|s| s.to_string(Some(k), false)) .unwrap_or_default(); - len += value.chars().count(); - if offset <= len { + char_count += value.chars().count(); + if char_offset <= char_count { return Some(CompletionType::Key { key: k.clone(), prefix: None, @@ -128,8 +129,8 @@ fn get_completion_type( } } if let Some(suffix) = &k.suffix { - len += suffix.chars().count(); - if offset <= len { + char_count += suffix.chars().count(); + if char_offset <= char_count { return Some(CompletionType::Literal(suffix.clone())); } } @@ -449,49 +450,6 @@ impl ModuleRegistry { } } - fn complete_literal( - &self, - s: String, - completions: &mut HashMap, - current_specifier: &str, - offset: usize, - range: &lsp::Range, - ) { - let label = if s.starts_with('/') { - s[0..].to_string() - } else { - s.to_string() - }; - let full_text = format!( - "{}{}{}", - ¤t_specifier[..offset], - s, - ¤t_specifier[offset..] - ); - let text_edit = Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: *range, - new_text: full_text.clone(), - })); - let filter_text = Some(full_text); - completions.insert( - s, - lsp::CompletionItem { - label, - kind: Some(lsp::CompletionItemKind::FOLDER), - filter_text, - sort_text: Some("1".to_string()), - text_edit, - commit_characters: Some( - REGISTRY_IMPORT_COMMIT_CHARS - .iter() - .map(|&c| c.into()) - .collect(), - ), - ..Default::default() - }, - ); - } - /// Disable a registry, removing its configuration, if any, from memory. pub fn disable(&mut self, origin: &str) { let Ok(origin_url) = Url::parse(origin) else { @@ -654,339 +612,366 @@ impl ModuleRegistry { /// any, for the specifier. pub async fn get_completions( &self, - current_specifier: &str, - offset: usize, + text: &str, range: &lsp::Range, + resolved: Option<&ModuleSpecifier>, specifier_exists: impl Fn(&ModuleSpecifier) -> bool, ) -> Option { - if let Ok(specifier) = Url::parse(current_specifier) { - let origin = base_url(&specifier); - let origin_len = origin.chars().count(); - if offset >= origin_len { - if let Some(registries) = self.origins.get(&origin) { - let path = &specifier[Position::BeforePath..]; - let path_offset = offset - origin_len; - let mut completions = HashMap::::new(); - let mut is_incomplete = false; - let mut did_match = false; - for registry in registries { - let tokens = parse(®istry.schema, None) - .map_err(|e| { - error!( - "Error parsing registry schema for origin \"{}\". {}", - origin, e - ); - }) - .ok()?; - let mut i = tokens.len(); - let last_key_name = StringOrNumber::String( - tokens - .iter() - .last() - .map(|t| { - if let Token::Key(key) = t { - if let StringOrNumber::String(s) = &key.name { - return s.clone(); - } - } - "".to_string() - }) - .unwrap_or_default(), - ); - loop { - let matcher = Matcher::new(&tokens[..i], None) - .map_err(|e| { - error!( - "Error creating matcher for schema for origin \"{}\". {}", - origin, e - ); - }) - .ok()?; - if let Some(match_result) = matcher.matches(path) { - did_match = true; - let completion_type = - get_completion_type(path_offset, &tokens, &match_result); - match completion_type { - Some(CompletionType::Literal(s)) => self.complete_literal( - s, - &mut completions, - current_specifier, - offset, - range, - ), - Some(CompletionType::Key { key, prefix, index }) => { - let maybe_url = registry.get_url_for_key(&key); - if let Some(url) = maybe_url { - if let Some(items) = self - .get_variable_items( - &key, - url, - &specifier, - &tokens, - &match_result, - ) - .await - { - let compiler = Compiler::new(&tokens[..=index], None); - let base = Url::parse(&origin).ok()?; - let (items, preselect, incomplete) = match items { - VariableItems::List(list) => { - (list.items, list.preselect, list.is_incomplete) - } - VariableItems::Simple(items) => (items, None, false), - }; - if incomplete { - is_incomplete = true; - } - for (idx, item) in items.into_iter().enumerate() { - let mut label = if let Some(p) = &prefix { - format!("{p}{item}") - } else { - item.clone() - }; - if label.ends_with('/') { - label.pop(); - } - let kind = if key.name == last_key_name - && !item.ends_with('/') - { - Some(lsp::CompletionItemKind::FILE) - } else { - Some(lsp::CompletionItemKind::FOLDER) - }; - let mut params = match_result.params.clone(); - params.insert( - key.name.clone(), - StringOrVec::from_str(&item, &key), - ); - let mut path = - compiler.to_path(¶ms).unwrap_or_default(); - if path.ends_with('/') { - path.pop(); - } - let item_specifier = base.join(&path).ok()?; - let full_text = item_specifier.as_str(); - let text_edit = Some(lsp::CompletionTextEdit::Edit( - lsp::TextEdit { - range: *range, - new_text: full_text.to_string(), - }, - )); - let command = if key.name == last_key_name - && !item.ends_with('/') - && !specifier_exists(&item_specifier) - { - Some(lsp::Command { - title: "".to_string(), - command: "deno.cache".to_string(), - arguments: Some(vec![ - json!([item_specifier]), - json!(&specifier), - ]), - }) - } else { - None - }; - let detail = Some(format!("({})", key.name)); - let filter_text = Some(full_text.to_string()); - let sort_text = Some(format!("{:0>10}", idx + 1)); - let preselect = - get_preselect(item.clone(), preselect.clone()); - let data = get_data_with_match( - registry, - &specifier, - &tokens, - &match_result, - &key, - &item, - ); - let commit_characters = if is_incomplete { - Some( - REGISTRY_IMPORT_COMMIT_CHARS - .iter() - .map(|&c| c.into()) - .collect(), - ) - } else { - Some( - IMPORT_COMMIT_CHARS - .iter() - .map(|&c| c.into()) - .collect(), - ) - }; - completions.insert( - item, - lsp::CompletionItem { - label, - kind, - detail, - sort_text, - filter_text, - text_edit, - command, - preselect, - data, - commit_characters, - ..Default::default() - }, - ); - } - } - } - } - None => (), - } - break; + let resolved = resolved + .map(Cow::Borrowed) + .or_else(|| ModuleSpecifier::parse(text).ok().map(Cow::Owned))?; + let resolved_str = resolved.as_str(); + let origin = base_url(&resolved); + let origin_char_count = origin.chars().count(); + let registries = self.origins.get(&origin)?; + let path = &resolved[Position::BeforePath..]; + let path_char_offset = resolved_str.chars().count() - origin_char_count; + let mut completions = HashMap::::new(); + let mut is_incomplete = false; + let mut did_match = false; + for registry in registries { + let tokens = parse(®istry.schema, None) + .map_err(|e| { + error!( + "Error parsing registry schema for origin \"{}\". {}", + origin, e + ); + }) + .ok()?; + let mut i = tokens.len(); + let last_key_name = StringOrNumber::String( + tokens + .iter() + .last() + .map(|t| { + if let Token::Key(key) = t { + if let StringOrNumber::String(s) = &key.name { + return s.clone(); } - i -= 1; - // If we have fallen though to the first token, and we still - // didn't get a match - if i == 0 { - match &tokens[i] { - // so if the first token is a string literal, we will return - // that as a suggestion - Token::String(s) => { - if s.starts_with(path) { - let label = s.to_string(); + } + "".to_string() + }) + .unwrap_or_default(), + ); + loop { + let matcher = Matcher::new(&tokens[..i], None) + .map_err(|e| { + error!( + "Error creating matcher for schema for origin \"{}\". {}", + origin, e + ); + }) + .ok()?; + if let Some(match_result) = matcher.matches(path) { + did_match = true; + let completion_type = + get_completion_type(path_char_offset, &tokens, &match_result); + match completion_type { + Some(CompletionType::Literal(s)) => { + let label = s; + let full_text = format!("{text}{label}"); + let text_edit = + Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: *range, + new_text: full_text.clone(), + })); + let filter_text = Some(full_text); + completions.insert( + label.clone(), + lsp::CompletionItem { + label, + kind: Some(lsp::CompletionItemKind::FOLDER), + filter_text, + sort_text: Some("1".to_string()), + text_edit, + commit_characters: Some( + REGISTRY_IMPORT_COMMIT_CHARS + .iter() + .map(|&c| c.into()) + .collect(), + ), + ..Default::default() + }, + ); + } + Some(CompletionType::Key { key, prefix, index }) => { + let maybe_url = registry.get_url_for_key(&key); + if let Some(url) = maybe_url { + if let Some(items) = self + .get_variable_items( + &key, + url, + &resolved, + &tokens, + &match_result, + ) + .await + { + let compiler = Compiler::new(&tokens[..=index], None); + let base = Url::parse(&origin).ok()?; + let (items, preselect, incomplete) = match items { + VariableItems::List(list) => { + (list.items, list.preselect, list.is_incomplete) + } + VariableItems::Simple(items) => (items, None, false), + }; + if incomplete { + is_incomplete = true; + } + for (idx, item) in items.into_iter().enumerate() { + let mut label = if let Some(p) = &prefix { + format!("{p}{item}") + } else { + item.clone() + }; + if label.ends_with('/') { + label.pop(); + } + let kind = + if key.name == last_key_name && !item.ends_with('/') { + Some(lsp::CompletionItemKind::FILE) + } else { + Some(lsp::CompletionItemKind::FOLDER) + }; + let mut params = match_result.params.clone(); + params.insert( + key.name.clone(), + StringOrVec::from_str(&item, &key), + ); + let mut path = + compiler.to_path(¶ms).unwrap_or_default(); + if path.ends_with('/') { + path.pop(); + } + let item_specifier = base.join(&path).ok()?; + let full_text = if let Some(suffix) = + item_specifier.as_str().strip_prefix(resolved_str) + { + format!("{text}{suffix}") + } else { + item_specifier.to_string() + }; + let text_edit = + Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: *range, + new_text: full_text.to_string(), + })); + let command = if key.name == last_key_name + && !item.ends_with('/') + && !specifier_exists(&item_specifier) + { + Some(lsp::Command { + title: "".to_string(), + command: "deno.cache".to_string(), + arguments: Some(vec![ + json!([item_specifier]), + json!(&resolved), + ]), + }) + } else { + None + }; + let detail = Some(format!("({})", key.name)); + let filter_text = Some(full_text.to_string()); + let sort_text = Some(format!("{:0>10}", idx + 1)); + let preselect = + get_preselect(item.clone(), preselect.clone()); + let data = get_data_with_match( + registry, + &resolved, + &tokens, + &match_result, + &key, + &item, + ); + let commit_characters = if is_incomplete { + Some( + REGISTRY_IMPORT_COMMIT_CHARS + .iter() + .map(|&c| c.into()) + .collect(), + ) + } else { + Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(), + ) + }; + completions.insert( + item, + lsp::CompletionItem { + label, + kind, + detail, + sort_text, + filter_text, + text_edit, + command, + preselect, + data, + commit_characters, + ..Default::default() + }, + ); + } + } + } + } + None => (), + } + break; + } + i -= 1; + // If we have fallen though to the first token, and we still + // didn't get a match + if i == 0 { + match &tokens[i] { + // so if the first token is a string literal, we will return + // that as a suggestion + Token::String(s) => { + if s.starts_with(path) { + let label = s.to_string(); + let kind = Some(lsp::CompletionItemKind::FOLDER); + let mut url = resolved.as_ref().clone(); + url.set_path(s); + let full_text = if let Some(suffix) = + url.as_str().strip_prefix(resolved_str) + { + format!("{text}{suffix}") + } else { + url.to_string() + }; + let text_edit = + Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: *range, + new_text: full_text.to_string(), + })); + let filter_text = Some(full_text.to_string()); + completions.insert( + s.to_string(), + lsp::CompletionItem { + label, + kind, + filter_text, + sort_text: Some("1".to_string()), + text_edit, + preselect: Some(true), + commit_characters: Some( + REGISTRY_IMPORT_COMMIT_CHARS + .iter() + .map(|&c| c.into()) + .collect(), + ), + ..Default::default() + }, + ); + } + } + // if the token though is a key, and the key has a prefix, and + // the path matches the prefix, we will go and get the items + // for that first key and return them. + Token::Key(k) => { + if let Some(prefix) = &k.prefix { + let maybe_url = registry.get_url_for_key(k); + if let Some(url) = maybe_url { + if let Some(items) = self.get_items(url).await { + let base = Url::parse(&origin).ok()?; + let (items, preselect, incomplete) = match items { + VariableItems::List(list) => { + (list.items, list.preselect, list.is_incomplete) + } + VariableItems::Simple(items) => (items, None, false), + }; + if incomplete { + is_incomplete = true; + } + for (idx, item) in items.into_iter().enumerate() { + let path = format!("{prefix}{item}"); let kind = Some(lsp::CompletionItemKind::FOLDER); - let mut url = specifier.clone(); - url.set_path(s); - let full_text = url.as_str(); + let item_specifier = base.join(&path).ok()?; + let full_text = if let Some(suffix) = + item_specifier.as_str().strip_prefix(resolved_str) + { + format!("{text}{suffix}") + } else { + item_specifier.to_string() + }; let text_edit = Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { range: *range, - new_text: full_text.to_string(), + new_text: full_text.clone(), })); + let command = if k.name == last_key_name + && !specifier_exists(&item_specifier) + { + Some(lsp::Command { + title: "".to_string(), + command: "deno.cache".to_string(), + arguments: Some(vec![ + json!([item_specifier]), + json!(&resolved), + ]), + }) + } else { + None + }; + let detail = Some(format!("({})", k.name)); let filter_text = Some(full_text.to_string()); + let sort_text = Some(format!("{:0>10}", idx + 1)); + let preselect = + get_preselect(item.clone(), preselect.clone()); + let data = get_data(registry, &resolved, k, &path); + let commit_characters = if is_incomplete { + Some( + REGISTRY_IMPORT_COMMIT_CHARS + .iter() + .map(|&c| c.into()) + .collect(), + ) + } else { + Some( + IMPORT_COMMIT_CHARS + .iter() + .map(|&c| c.into()) + .collect(), + ) + }; completions.insert( - s.to_string(), + item.clone(), lsp::CompletionItem { - label, + label: item, kind, + detail, + sort_text, filter_text, - sort_text: Some("1".to_string()), text_edit, - preselect: Some(true), - commit_characters: Some( - REGISTRY_IMPORT_COMMIT_CHARS - .iter() - .map(|&c| c.into()) - .collect(), - ), + command, + preselect, + data, + commit_characters, ..Default::default() }, ); } } - // if the token though is a key, and the key has a prefix, and - // the path matches the prefix, we will go and get the items - // for that first key and return them. - Token::Key(k) => { - if let Some(prefix) = &k.prefix { - let maybe_url = registry.get_url_for_key(k); - if let Some(url) = maybe_url { - if let Some(items) = self.get_items(url).await { - let base = Url::parse(&origin).ok()?; - let (items, preselect, incomplete) = match items { - VariableItems::List(list) => { - (list.items, list.preselect, list.is_incomplete) - } - VariableItems::Simple(items) => { - (items, None, false) - } - }; - if incomplete { - is_incomplete = true; - } - for (idx, item) in items.into_iter().enumerate() { - let path = format!("{prefix}{item}"); - let kind = Some(lsp::CompletionItemKind::FOLDER); - let item_specifier = base.join(&path).ok()?; - let full_text = item_specifier.as_str(); - let text_edit = Some( - lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: *range, - new_text: full_text.to_string(), - }), - ); - let command = if k.name == last_key_name - && !specifier_exists(&item_specifier) - { - Some(lsp::Command { - title: "".to_string(), - command: "deno.cache".to_string(), - arguments: Some(vec![ - json!([item_specifier]), - json!(&specifier), - ]), - }) - } else { - None - }; - let detail = Some(format!("({})", k.name)); - let filter_text = Some(full_text.to_string()); - let sort_text = Some(format!("{:0>10}", idx + 1)); - let preselect = - get_preselect(item.clone(), preselect.clone()); - let data = get_data(registry, &specifier, k, &path); - let commit_characters = if is_incomplete { - Some( - REGISTRY_IMPORT_COMMIT_CHARS - .iter() - .map(|&c| c.into()) - .collect(), - ) - } else { - Some( - IMPORT_COMMIT_CHARS - .iter() - .map(|&c| c.into()) - .collect(), - ) - }; - completions.insert( - item.clone(), - lsp::CompletionItem { - label: item, - kind, - detail, - sort_text, - filter_text, - text_edit, - command, - preselect, - data, - commit_characters, - ..Default::default() - }, - ); - } - } - } - } - } } - break; } } } - // If we return None, other sources of completions will be looked for - // but if we did at least match part of a registry, we should send an - // empty vector so that no-completions will be sent back to the client - return if completions.is_empty() && !did_match { - None - } else { - Some(lsp::CompletionList { - items: completions.into_values().collect(), - is_incomplete, - }) - }; + break; } } } - - self.get_origin_completions(current_specifier, range) + // If we return None, other sources of completions will be looked for + // but if we did at least match part of a registry, we should send an + // empty vector so that no-completions will be sent back to the client + if completions.is_empty() && !did_match { + None + } else { + Some(lsp::CompletionList { + items: completions.into_values().collect(), + is_incomplete, + }) + } } pub async fn get_documentation( @@ -1316,9 +1301,7 @@ mod tests { character: 21, }, }; - let completions = module_registry - .get_completions("h", 1, &range, |_| false) - .await; + let completions = module_registry.get_origin_completions("h", &range); assert!(completions.is_some()); let completions = completions.unwrap().items; assert_eq!(completions.len(), 1); @@ -1340,9 +1323,8 @@ mod tests { character: 36, }, }; - let completions = module_registry - .get_completions("http://localhost", 16, &range, |_| false) - .await; + let completions = + module_registry.get_origin_completions("http://localhost", &range); assert!(completions.is_some()); let completions = completions.unwrap().items; assert_eq!(completions.len(), 1); @@ -1377,7 +1359,7 @@ mod tests { }, }; let completions = module_registry - .get_completions("http://localhost:4545", 21, &range, |_| false) + .get_completions("http://localhost:4545", &range, None, |_| false) .await; assert!(completions.is_some()); let completions = completions.unwrap().items; @@ -1393,7 +1375,7 @@ mod tests { }, }; let completions = module_registry - .get_completions("http://localhost:4545/", 22, &range, |_| false) + .get_completions("http://localhost:4545/", &range, None, |_| false) .await; assert!(completions.is_some()); let completions = completions.unwrap().items; @@ -1409,7 +1391,7 @@ mod tests { }, }; let completions = module_registry - .get_completions("http://localhost:4545/x/", 24, &range, |_| false) + .get_completions("http://localhost:4545/x/", &range, None, |_| false) .await; assert!(completions.is_some()); let completions = completions.unwrap(); @@ -1434,7 +1416,7 @@ mod tests { }, }; let completions = module_registry - .get_completions("http://localhost:4545/x/a", 25, &range, |_| false) + .get_completions("http://localhost:4545/x/a", &range, None, |_| false) .await; assert!(completions.is_some()); let completions = completions.unwrap(); @@ -1470,7 +1452,7 @@ mod tests { }, }; let completions = module_registry - .get_completions("http://localhost:4545/x/a@", 26, &range, |_| false) + .get_completions("http://localhost:4545/x/a@", &range, None, |_| false) .await; assert!(completions.is_some()); let completions = completions.unwrap().items; @@ -1493,7 +1475,7 @@ mod tests { }, }; let completions = module_registry - .get_completions("http://localhost:4545/x/a@v1.", 29, &range, |_| false) + .get_completions("http://localhost:4545/x/a@v1.", &range, None, |_| false) .await; assert!(completions.is_some()); let completions = completions.unwrap().items; @@ -1516,9 +1498,12 @@ mod tests { }, }; let completions = module_registry - .get_completions("http://localhost:4545/x/a@v1.0.0/", 33, &range, |_| { - false - }) + .get_completions( + "http://localhost:4545/x/a@v1.0.0/", + &range, + None, + |_| false, + ) .await; assert!(completions.is_some()); let completions = completions.unwrap().items; @@ -1541,9 +1526,12 @@ mod tests { }, }; let completions = module_registry - .get_completions("http://localhost:4545/x/a@v1.0.0/b", 34, &range, |_| { - false - }) + .get_completions( + "http://localhost:4545/x/a@v1.0.0/b", + &range, + None, + |_| false, + ) .await; assert!(completions.is_some()); let completions = completions.unwrap().items; @@ -1565,8 +1553,8 @@ mod tests { let completions = module_registry .get_completions( "http://localhost:4545/x/a@v1.0.0/b/", - 35, &range, + None, |_| false, ) .await; @@ -1602,7 +1590,7 @@ mod tests { }, }; let completions = module_registry - .get_completions("http://localhost:4545/", 22, &range, |_| false) + .get_completions("http://localhost:4545/", &range, None, |_| false) .await; assert!(completions.is_some()); let completions = completions.unwrap().items; @@ -1631,12 +1619,16 @@ mod tests { }, }; let completions = module_registry - .get_completions("http://localhost:4545/cde@", 26, &range, |_| false) + .get_completions("http://localhost:4545/cde@", &range, None, |_| false) .await; assert!(completions.is_some()); let completions = completions.unwrap().items; - assert_eq!(completions.len(), 2); for completion in completions { + if let Some(filter_text) = completion.filter_text { + if !"http://localhost:4545/cde@".contains(&filter_text) { + continue; + } + } assert!(completion.text_edit.is_some()); if let lsp::CompletionTextEdit::Edit(edit) = completion.text_edit.unwrap() { @@ -1674,7 +1666,7 @@ mod tests { }, }; let completions = module_registry - .get_completions("http://localhost:4545/", 22, &range, |_| false) + .get_completions("http://localhost:4545/", &range, None, |_| false) .await; assert!(completions.is_some()); let completions = completions.unwrap().items; @@ -1693,6 +1685,48 @@ mod tests { } } + #[tokio::test] + async fn test_registry_completions_import_map() { + let _g = test_util::http_server(); + let temp_dir = TempDir::new(); + let location = temp_dir.path().join("registries").to_path_buf(); + let mut module_registry = ModuleRegistry::new( + location, + Arc::new(HttpClientProvider::new(None, None)), + ); + module_registry.enable("http://localhost:4545/").await; + let range = lsp::Range { + start: lsp::Position { + line: 0, + character: 20, + }, + end: lsp::Position { + line: 0, + character: 33, + }, + }; + let completions = module_registry + .get_completions( + "localhost4545/", + &range, + Some(&ModuleSpecifier::parse("http://localhost:4545/").unwrap()), + |_| false, + ) + .await; + assert!(completions.is_some()); + let completions = completions.unwrap().items; + assert_eq!(completions.len(), 3); + for completion in completions { + assert!(completion.text_edit.is_some()); + if let lsp::CompletionTextEdit::Edit(edit) = completion.text_edit.unwrap() + { + assert_eq!(edit.new_text, format!("localhost4545{}", completion.label)); + } else { + unreachable!("unexpected text edit"); + } + } + } + #[test] fn test_parse_replacement_variables() { let actual = parse_replacement_variables(