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:
parent
fe884c557a
commit
3659781f88
2 changed files with 420 additions and 389 deletions
|
@ -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,
|
||||
|
|
|
@ -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!(
|
||||
"{}{}{}",
|
||||
¤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,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(
|
||||
|
|
Loading…
Reference in a new issue