1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-21 15:04:11 -05:00

feat(lsp): registry completions for import-mapped specifiers (#24792)

This commit is contained in:
Nayeem Rahman 2024-07-30 23:26:09 +01:00 committed by GitHub
parent fe884c557a
commit 3659781f88
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 420 additions and 389 deletions

View file

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

View file

@ -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<CompletionType> {
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<String, lsp::CompletionItem>,
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!(
"{}{}{}",
&current_specifier[..offset],
s,
&current_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,18 +612,20 @@ 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<lsp::CompletionList> {
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 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::<String, lsp::CompletionItem>::new();
let mut is_incomplete = false;
let mut did_match = false;
@ -705,15 +665,35 @@ impl ModuleRegistry {
if let Some(match_result) = matcher.matches(path) {
did_match = true;
let completion_type =
get_completion_type(path_offset, &tokens, &match_result);
get_completion_type(path_char_offset, &tokens, &match_result);
match completion_type {
Some(CompletionType::Literal(s)) => self.complete_literal(
s,
&mut completions,
current_specifier,
offset,
range,
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 {
@ -721,7 +701,7 @@ impl ModuleRegistry {
.get_variable_items(
&key,
url,
&specifier,
&resolved,
&tokens,
&match_result,
)
@ -747,9 +727,8 @@ impl ModuleRegistry {
if label.ends_with('/') {
label.pop();
}
let kind = if key.name == last_key_name
&& !item.ends_with('/')
{
let kind =
if key.name == last_key_name && !item.ends_with('/') {
Some(lsp::CompletionItemKind::FILE)
} else {
Some(lsp::CompletionItemKind::FOLDER)
@ -765,13 +744,18 @@ impl ModuleRegistry {
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 {
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)
@ -781,7 +765,7 @@ impl ModuleRegistry {
command: "deno.cache".to_string(),
arguments: Some(vec![
json!([item_specifier]),
json!(&specifier),
json!(&resolved),
]),
})
} else {
@ -794,7 +778,7 @@ impl ModuleRegistry {
get_preselect(item.clone(), preselect.clone());
let data = get_data_with_match(
registry,
&specifier,
&resolved,
&tokens,
&match_result,
&key,
@ -809,10 +793,7 @@ impl ModuleRegistry {
)
} else {
Some(
IMPORT_COMMIT_CHARS
.iter()
.map(|&c| c.into())
.collect(),
IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(),
)
};
completions.insert(
@ -850,9 +831,15 @@ impl ModuleRegistry {
if s.starts_with(path) {
let label = s.to_string();
let kind = Some(lsp::CompletionItemKind::FOLDER);
let mut url = specifier.clone();
let mut url = resolved.as_ref().clone();
url.set_path(s);
let full_text = url.as_str();
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,
@ -892,9 +879,7 @@ impl ModuleRegistry {
VariableItems::List(list) => {
(list.items, list.preselect, list.is_incomplete)
}
VariableItems::Simple(items) => {
(items, None, false)
}
VariableItems::Simple(items) => (items, None, false),
};
if incomplete {
is_incomplete = true;
@ -903,13 +888,18 @@ impl ModuleRegistry {
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 {
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)
{
@ -918,7 +908,7 @@ impl ModuleRegistry {
command: "deno.cache".to_string(),
arguments: Some(vec![
json!([item_specifier]),
json!(&specifier),
json!(&resolved),
]),
})
} else {
@ -929,7 +919,7 @@ impl ModuleRegistry {
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 data = get_data(registry, &resolved, k, &path);
let commit_characters = if is_incomplete {
Some(
REGISTRY_IMPORT_COMMIT_CHARS
@ -974,20 +964,15 @@ impl ModuleRegistry {
// 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 {
if completions.is_empty() && !did_match {
None
} else {
Some(lsp::CompletionList {
items: completions.into_values().collect(),
is_incomplete,
})
};
}
}
}
self.get_origin_completions(current_specifier, range)
}
pub async fn get_documentation(
&self,
@ -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(