0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-10-29 08:58:01 -04:00

fix(lsp): correctly parse registry patterns (#12063)

This commit is contained in:
Kitson Kelly 2021-09-14 22:40:35 +10:00 committed by GitHub
parent 5e2c5d0afa
commit d36b01ff69
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 369 additions and 60 deletions

View file

@ -68,7 +68,7 @@ async fn check_auto_config_registry(
let origin = specifier.origin().ascii_serialization(); let origin = specifier.origin().ascii_serialization();
let suggestions = snapshot let suggestions = snapshot
.module_registries .module_registries
.fetch_config(&origin) .check_origin(&origin)
.await .await
.is_ok(); .is_ok();
client client

View file

@ -72,7 +72,11 @@ fn base_url(url: &Url) -> String {
#[derive(Debug)] #[derive(Debug)]
enum CompletorType { enum CompletorType {
Literal(String), Literal(String),
Key(Key, Option<String>), Key {
key: Key,
prefix: Option<String>,
index: usize,
},
} }
/// Determine if a completion at a given offset is a string literal or a key/ /// Determine if a completion at a given offset is a string literal or a key/
@ -83,7 +87,7 @@ fn get_completor_type(
match_result: &MatchResult, match_result: &MatchResult,
) -> Option<CompletorType> { ) -> Option<CompletorType> {
let mut len = 0_usize; let mut len = 0_usize;
for token in tokens { for (index, token) in tokens.iter().enumerate() {
match token { match token {
Token::String(s) => { Token::String(s) => {
len += s.chars().count(); len += s.chars().count();
@ -95,7 +99,11 @@ fn get_completor_type(
if let Some(prefix) = &k.prefix { if let Some(prefix) = &k.prefix {
len += prefix.chars().count(); len += prefix.chars().count();
if offset < len { if offset < len {
return Some(CompletorType::Key(k.clone(), Some(prefix.clone()))); return Some(CompletorType::Key {
key: k.clone(),
prefix: Some(prefix.clone()),
index,
});
} }
} }
if offset < len { if offset < len {
@ -108,7 +116,11 @@ fn get_completor_type(
.unwrap_or_default(); .unwrap_or_default();
len += value.chars().count(); len += value.chars().count();
if offset <= len { if offset <= len {
return Some(CompletorType::Key(k.clone(), None)); return Some(CompletorType::Key {
key: k.clone(),
prefix: None,
index,
});
} }
} }
if let Some(suffix) = &k.suffix { if let Some(suffix) = &k.suffix {
@ -234,6 +246,18 @@ pub(crate) struct RegistryConfiguration {
variables: Vec<RegistryConfigurationVariable>, variables: Vec<RegistryConfigurationVariable>,
} }
impl RegistryConfiguration {
fn get_url_for_key(&self, key: &Key) -> Option<&str> {
self.variables.iter().find_map(|v| {
if key.name == StringOrNumber::String(v.key.clone()) {
Some(v.url.as_str())
} else {
None
}
})
}
}
/// A structure that represents the configuration of an origin and its module /// A structure that represents the configuration of an origin and its module
/// registries. /// registries.
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -341,16 +365,26 @@ impl ModuleRegistry {
Ok(()) Ok(())
} }
/// Attempt to fetch the configuration for a specific origin. /// Check to see if the given origin has a registry configuration.
pub(crate) async fn fetch_config( pub(crate) async fn check_origin(
&self, &self,
origin: &str, origin: &str,
) -> Result<Vec<RegistryConfiguration>, AnyError> { ) -> Result<(), AnyError> {
let origin_url = Url::parse(origin)?; let origin_url = Url::parse(origin)?;
let specifier = origin_url.join(CONFIG_PATH)?; let specifier = origin_url.join(CONFIG_PATH)?;
self.fetch_config(&specifier).await?;
Ok(())
}
/// Fetch and validate the specifier to a registry configuration, resolving
/// with the configuration if valid.
async fn fetch_config(
&self,
specifier: &ModuleSpecifier,
) -> Result<Vec<RegistryConfiguration>, AnyError> {
let file = self let file = self
.file_fetcher .file_fetcher
.fetch(&specifier, &mut Permissions::allow_all()) .fetch(specifier, &mut Permissions::allow_all())
.await?; .await?;
let config: RegistryConfigurationJson = serde_json::from_str(&file.source)?; let config: RegistryConfigurationJson = serde_json::from_str(&file.source)?;
validate_config(&config)?; validate_config(&config)?;
@ -360,11 +394,28 @@ impl ModuleRegistry {
/// Enable a registry by attempting to retrieve its configuration and /// Enable a registry by attempting to retrieve its configuration and
/// validating it. /// validating it.
pub async fn enable(&mut self, origin: &str) -> Result<(), AnyError> { pub async fn enable(&mut self, origin: &str) -> Result<(), AnyError> {
let origin = base_url(&Url::parse(origin)?); let origin_url = Url::parse(origin)?;
let origin = base_url(&origin_url);
#[allow(clippy::map_entry)] #[allow(clippy::map_entry)]
// we can't use entry().or_insert_with() because we can't use async closures // we can't use entry().or_insert_with() because we can't use async closures
if !self.origins.contains_key(&origin) { if !self.origins.contains_key(&origin) {
let configs = self.fetch_config(&origin).await?; let specifier = origin_url.join(CONFIG_PATH)?;
let configs = self.fetch_config(&specifier).await?;
self.origins.insert(origin, configs);
}
Ok(())
}
#[cfg(test)]
/// This is only used during testing, as it directly provides the full URL
/// for obtaining the registry configuration, versus "guessing" at it.
async fn enable_custom(&mut self, specifier: &str) -> Result<(), AnyError> {
let specifier = Url::parse(specifier)?;
let origin = base_url(&specifier);
#[allow(clippy::map_entry)]
if !self.origins.contains_key(&origin) {
let configs = self.fetch_config(&specifier).await?;
self.origins.insert(origin, configs); self.origins.insert(origin, configs);
} }
@ -432,46 +483,34 @@ impl ModuleRegistry {
offset, offset,
range, range,
), ),
Some(CompletorType::Key(k, p)) => { Some(CompletorType::Key { key, prefix, index }) => {
let maybe_url = registry.variables.iter().find_map(|v| { let maybe_url = registry.get_url_for_key(&key);
if k.name == StringOrNumber::String(v.key.clone()) {
Some(v.url.as_str())
} else {
None
}
});
if let Some(url) = maybe_url { if let Some(url) = maybe_url {
if let Some(items) = self if let Some(items) = self
.get_variable_items(url, &tokens, &match_result) .get_variable_items(url, &tokens, &match_result)
.await .await
{ {
let end = if p.is_some() { i + 1 } else { i }; let compiler = Compiler::new(&tokens[..=index], None);
let end = if end > tokens.len() { let base = Url::parse(&origin).ok()?;
tokens.len()
} else {
end
};
let compiler = Compiler::new(&tokens[..end], None);
for (idx, item) in items.into_iter().enumerate() { for (idx, item) in items.into_iter().enumerate() {
let label = if let Some(p) = &p { let label = if let Some(p) = &prefix {
format!("{}{}", p, item) format!("{}{}", p, item)
} else { } else {
item.clone() item.clone()
}; };
let kind = if k.name == last_key_name { let kind = if key.name == last_key_name {
Some(lsp::CompletionItemKind::File) Some(lsp::CompletionItemKind::File)
} else { } else {
Some(lsp::CompletionItemKind::Folder) Some(lsp::CompletionItemKind::Folder)
}; };
let mut params = match_result.params.clone(); let mut params = match_result.params.clone();
params.insert( params.insert(
k.name.clone(), key.name.clone(),
StringOrVec::from_str(&item, &k), StringOrVec::from_str(&item, &key),
); );
let path = let path =
compiler.to_path(&params).unwrap_or_default(); compiler.to_path(&params).unwrap_or_default();
let mut item_specifier = Url::parse(&origin).ok()?; let item_specifier = base.join(&path).ok()?;
item_specifier.set_path(&path);
let full_text = item_specifier.as_str(); let full_text = item_specifier.as_str();
let text_edit = Some(lsp::CompletionTextEdit::Edit( let text_edit = Some(lsp::CompletionTextEdit::Edit(
lsp::TextEdit { lsp::TextEdit {
@ -479,7 +518,7 @@ impl ModuleRegistry {
new_text: full_text.to_string(), new_text: full_text.to_string(),
}, },
)); ));
let command = if k.name == last_key_name let command = if key.name == last_key_name
&& !state_snapshot && !state_snapshot
.sources .sources
.contains_key(&item_specifier) .contains_key(&item_specifier)
@ -492,7 +531,7 @@ impl ModuleRegistry {
} else { } else {
None None
}; };
let detail = Some(format!("({})", k.name)); let detail = Some(format!("({})", key.name));
let filter_text = Some(full_text.to_string()); let filter_text = Some(full_text.to_string());
let sort_text = Some(format!("{:0>10}", idx + 1)); let sort_text = Some(format!("{:0>10}", idx + 1));
completions.insert( completions.insert(
@ -518,10 +557,12 @@ impl ModuleRegistry {
} }
i -= 1; i -= 1;
// If we have fallen though to the first token, and we still // If we have fallen though to the first token, and we still
// didn't get a match, but the first token is a string literal, we // didn't get a match
// need to suggest the string literal.
if i == 0 { if i == 0 {
if let Token::String(s) = &tokens[i] { 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) { if s.starts_with(path) {
let label = s.to_string(); let label = s.to_string();
let kind = Some(lsp::CompletionItemKind::Folder); let kind = Some(lsp::CompletionItemKind::Folder);
@ -547,6 +588,61 @@ impl ModuleRegistry {
); );
} }
} }
// 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()?;
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
&& !state_snapshot
.sources
.contains_key(&item_specifier)
{
Some(lsp::Command {
title: "".to_string(),
command: "deno.cache".to_string(),
arguments: Some(vec![json!([item_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));
completions.insert(
item.clone(),
lsp::CompletionItem {
label: item,
kind,
detail,
sort_text,
filter_text,
text_edit,
command,
..Default::default()
},
);
}
}
}
}
}
}
break; break;
} }
} }
@ -604,6 +700,30 @@ impl ModuleRegistry {
} }
} }
async fn get_items(&self, url: &str) -> Option<Vec<String>> {
let specifier = ModuleSpecifier::parse(url).ok()?;
let file = self
.file_fetcher
.fetch(&specifier, &mut Permissions::allow_all())
.await
.map_err(|err| {
error!(
"Internal error fetching endpoint \"{}\". {}",
specifier, err
);
})
.ok()?;
let items: Vec<String> = serde_json::from_str(&file.source)
.map_err(|err| {
error!(
"Error parsing response from endpoint \"{}\". {}",
specifier, err
);
})
.ok()?;
Some(items)
}
async fn get_variable_items( async fn get_variable_items(
&self, &self,
url: &str, url: &str,
@ -961,6 +1081,122 @@ mod tests {
assert!(completions[1].command.is_some()); assert!(completions[1].command.is_some());
} }
#[tokio::test]
async fn test_registry_completions_key_first() {
let _g = test_util::http_server();
let temp_dir = TempDir::new().expect("could not create tmp");
let location = temp_dir.path().join("registries");
let mut module_registry = ModuleRegistry::new(&location);
module_registry
.enable_custom("http://localhost:4545/lsp/registries/deno-import-intellisense-key-first.json")
.await
.expect("could not enable");
let state_snapshot = setup(&[]);
let range = lsp::Range {
start: lsp::Position {
line: 0,
character: 20,
},
end: lsp::Position {
line: 0,
character: 42,
},
};
let completions = module_registry
.get_completions("http://localhost:4545/", 22, &range, &state_snapshot)
.await;
assert!(completions.is_some());
let completions = completions.unwrap();
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!("http://localhost:4545/{}", completion.label)
);
} else {
unreachable!("unexpected text edit");
}
}
let range = lsp::Range {
start: lsp::Position {
line: 0,
character: 20,
},
end: lsp::Position {
line: 0,
character: 46,
},
};
let completions = module_registry
.get_completions(
"http://localhost:4545/cde@",
26,
&range,
&state_snapshot,
)
.await;
assert!(completions.is_some());
let completions = completions.unwrap();
assert_eq!(completions.len(), 2);
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!("http://localhost:4545/cde@{}", completion.label)
);
} else {
unreachable!("unexpected text edit");
}
}
}
#[tokio::test]
async fn test_registry_completions_complex() {
let _g = test_util::http_server();
let temp_dir = TempDir::new().expect("could not create tmp");
let location = temp_dir.path().join("registries");
let mut module_registry = ModuleRegistry::new(&location);
module_registry
.enable_custom("http://localhost:4545/lsp/registries/deno-import-intellisense-complex.json")
.await
.expect("could not enable");
let state_snapshot = setup(&[]);
let range = lsp::Range {
start: lsp::Position {
line: 0,
character: 20,
},
end: lsp::Position {
line: 0,
character: 42,
},
};
let completions = module_registry
.get_completions("http://localhost:4545/", 22, &range, &state_snapshot)
.await;
assert!(completions.is_some());
let completions = completions.unwrap();
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!("http://localhost:4545/{}", completion.label)
);
} else {
unreachable!("unexpected text edit");
}
}
}
#[test] #[test]
fn test_parse_replacement_variables() { fn test_parse_replacement_variables() {
let actual = parse_replacement_variables( let actual = parse_replacement_variables(

View file

@ -0,0 +1,4 @@
[
"1.0.0",
"1.0.1"
]

View file

@ -0,0 +1,4 @@
[
"2.0.0",
"2.0.1"
]

View file

@ -0,0 +1,5 @@
[
"efg",
"efgh",
"fg"
]

View file

@ -0,0 +1,6 @@
[
"0.2.2",
"0.2.1",
"0.2.0",
"0.1.0"
]

View file

@ -0,0 +1,6 @@
[
"mod.ts",
"example/mod.ts",
"CHANGELOG.md",
"deps.ts"
]

View file

@ -0,0 +1,3 @@
[
"3.0.0"
]

View file

@ -0,0 +1,22 @@
{
"version": 1,
"registries": [
{
"schema": "/:module([a-zA-Z0-9_]*)@:version/:path*",
"variables": [
{
"key": "module",
"url": "http://localhost:4545/lsp/registries/complex.json"
},
{
"key": "version",
"url": "http://localhost:4545/lsp/registries/complex_${module}.json"
},
{
"key": "path",
"url": "http://localhost:4545/lsp/registries/complex_${module}_${version}.json"
}
]
}
]
}

View file

@ -0,0 +1,18 @@
{
"version": 1,
"registries": [
{
"schema": "/:module([a-zA-Z0-9-_]+)@:tag([a-zA-Z0-9-_\\.]+)",
"variables": [
{
"key": "module",
"url": "http://localhost:4545/lsp/registries/key_first.json"
},
{
"key": "tag",
"url": "http://localhost:4545/lsp/registries/${module}_tags.json"
}
]
}
]
}

View file

@ -0,0 +1,5 @@
[
"cde",
"cdef",
"def"
]