summaryrefslogtreecommitdiff
path: root/cli/lsp/registries.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/lsp/registries.rs')
-rw-r--r--cli/lsp/registries.rs351
1 files changed, 311 insertions, 40 deletions
diff --git a/cli/lsp/registries.rs b/cli/lsp/registries.rs
index 0703e468a..62a6aa987 100644
--- a/cli/lsp/registries.rs
+++ b/cli/lsp/registries.rs
@@ -22,6 +22,8 @@ use deno_core::resolve_url;
use deno_core::serde::Deserialize;
use deno_core::serde_json;
use deno_core::serde_json::json;
+use deno_core::serde_json::Value;
+use deno_core::url::ParseError;
use deno_core::url::Position;
use deno_core::url::Url;
use deno_core::ModuleSpecifier;
@@ -135,23 +137,60 @@ fn get_completor_type(
None
}
-/// Convert a completion URL string from a completions configuration into a
-/// fully qualified URL which can be fetched to provide the completions.
-fn get_completion_endpoint(
+/// Generate a data value for a completion item that will instruct the client to
+/// resolve the completion item to obtain further information, in this case, the
+/// details/documentation endpoint for the item if it exists in the registry
+/// configuration
+fn get_data(
+ registry: &RegistryConfiguration,
+ base: &ModuleSpecifier,
+ variable: &Key,
+ value: &str,
+) -> Option<Value> {
+ let url = registry.get_documentation_url_for_key(variable)?;
+ get_endpoint(url, base, variable, Some(value))
+ .ok()
+ .map(|specifier| json!({ "documentation": specifier }))
+}
+
+/// Convert a single variable templated string into a fully qualified URL which
+/// can be fetched to provide additional data.
+fn get_endpoint(
+ url: &str,
+ base: &Url,
+ variable: &Key,
+ maybe_value: Option<&str>,
+) -> Result<ModuleSpecifier, AnyError> {
+ let url = replace_variable(url, variable, maybe_value);
+ parse_url_with_base(&url, base)
+}
+
+/// Convert a templated URL string into a fully qualified URL which can be
+/// fetched to provide additional data. If `maybe_value` is some, then the
+/// variable will replaced in the template prior to other matched variables
+/// being replaced, otherwise the supplied variable will be blanked out if
+/// present in the template.
+fn get_endpoint_with_match(
+ variable: &Key,
url: &str,
+ base: &Url,
tokens: &[Token],
match_result: &MatchResult,
+ maybe_value: Option<&str>,
) -> Result<ModuleSpecifier, AnyError> {
- let mut url_str = url.to_string();
+ let mut url = url.to_string();
+ let has_value = maybe_value.is_some();
+ if has_value {
+ url = replace_variable(&url, variable, maybe_value);
+ }
for (key, value) in match_result.params.iter() {
if let StringOrNumber::String(name) = key {
let maybe_key = tokens.iter().find_map(|t| match t {
Token::Key(k) if k.name == *key => Some(k),
_ => None,
});
- url_str =
- url_str.replace(&format!("${{{}}}", name), &value.to_string(maybe_key));
- url_str = url_str.replace(
+ url = url.replace(&format!("${{{}}}", name), &value.to_string(maybe_key));
+ url = url.replace(
&format!("${{{{{}}}}}", name),
&percent_encoding::percent_encode(
value.to_string(maybe_key).as_bytes(),
@@ -161,7 +200,20 @@ fn get_completion_endpoint(
);
}
}
- resolve_url(&url_str).map_err(|err| err.into())
+ if !has_value {
+ url = replace_variable(&url, variable, None);
+ }
+ parse_url_with_base(&url, base)
+}
+
+/// Based on the preselect response from the registry, determine if this item
+/// should be preselected or not.
+fn get_preselect(item: String, preselect: Option<String>) -> Option<bool> {
+ if Some(item) == preselect {
+ Some(true)
+ } else {
+ None
+ }
}
fn parse_replacement_variables<S: AsRef<str>>(s: S) -> Vec<String> {
@@ -171,11 +223,44 @@ fn parse_replacement_variables<S: AsRef<str>>(s: S) -> Vec<String> {
.collect()
}
+/// Attempt to parse a URL along with a base, where the base will be used if the
+/// URL requires one.
+fn parse_url_with_base(
+ url: &str,
+ base: &ModuleSpecifier,
+) -> Result<ModuleSpecifier, AnyError> {
+ match Url::parse(url) {
+ Ok(url) => Ok(url),
+ Err(ParseError::RelativeUrlWithoutBase) => {
+ base.join(url).map_err(|err| err.into())
+ }
+ Err(err) => Err(err.into()),
+ }
+}
+
+/// Replaces a variable in a templated URL string with the supplied value or
+/// "blank" it out if there is no value supplied.
+fn replace_variable(
+ url: &str,
+ variable: &Key,
+ maybe_value: Option<&str>,
+) -> String {
+ let url_str = url.to_string();
+ let value = maybe_value.unwrap_or("");
+ if let StringOrNumber::String(name) = &variable.name {
+ url_str
+ .replace(&format!("${{{}}}", name), value)
+ .replace(&format! {"${{{{{}}}}}", name}, value)
+ } else {
+ url_str
+ }
+}
+
/// Validate a registry configuration JSON structure.
fn validate_config(config: &RegistryConfigurationJson) -> Result<(), AnyError> {
- if config.version != 1 {
+ if config.version < 1 || config.version > 2 {
return Err(anyhow!(
- "Invalid registry configuration. Expected version 1 got {}.",
+ "Invalid registry configuration. Expected version 1 or 2 got {}.",
config.version
));
}
@@ -212,13 +297,13 @@ fn validate_config(config: &RegistryConfigurationJson) -> Result<(), AnyError> {
let replacement_variables = parse_replacement_variables(&variable.url);
let limited_keys = key_names.get(0..key_index).unwrap();
for v in replacement_variables {
- if variable.key == v {
+ if variable.key == v && config.version == 1 {
return Err(anyhow!("Invalid registry configuration. Url \"{}\" (for variable \"{}\" in registry with schema \"{}\") uses variable \"{}\", which is not allowed because that would be a self reference.", variable.url, variable.key, registry.schema, v));
}
let key_index = limited_keys.iter().position(|key| key == &v);
- if key_index.is_none() {
+ if key_index.is_none() && variable.key != v {
return Err(anyhow!("Invalid registry configuration. Url \"{}\" (for variable \"{}\" in registry with schema \"{}\") uses variable \"{}\", which is not allowed because the schema defines \"{}\" to the right of \"{}\".", variable.url, variable.key, registry.schema, v, v, variable.key));
}
}
@@ -232,6 +317,9 @@ fn validate_config(config: &RegistryConfigurationJson) -> Result<(), AnyError> {
pub(crate) struct RegistryConfigurationVariable {
/// The name of the variable.
key: String,
+ /// An optional URL/API endpoint that can provide optional documentation for a
+ /// completion item when requested by the language server.
+ documentation: Option<String>,
/// The URL with variable substitutions of the endpoint that will provide
/// completions for the variable.
url: String,
@@ -255,6 +343,16 @@ impl RegistryConfiguration {
}
})
}
+
+ fn get_documentation_url_for_key(&self, key: &Key) -> Option<&str> {
+ self.variables.iter().find_map(|v| {
+ if key.name == StringOrNumber::String(v.key.clone()) {
+ v.documentation.as_deref()
+ } else {
+ None
+ }
+ })
+ }
}
/// A structure that represents the configuration of an origin and its module
@@ -265,6 +363,22 @@ struct RegistryConfigurationJson {
registries: Vec<RegistryConfiguration>,
}
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct VariableItemsList {
+ pub items: Vec<String>,
+ #[serde(default)]
+ pub is_incomplete: bool,
+ pub preselect: Option<String>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(untagged)]
+enum VariableItems {
+ Simple(Vec<String>),
+ List(VariableItemsList),
+}
+
/// A structure which holds the information about currently configured module
/// registries and can provide completion information for URLs that match
/// one of the enabled registries.
@@ -444,7 +558,7 @@ impl ModuleRegistry {
offset: usize,
range: &lsp::Range,
specifier_exists: impl Fn(&ModuleSpecifier) -> bool,
- ) -> Option<Vec<lsp::CompletionItem>> {
+ ) -> Option<lsp::CompletionList> {
if let Ok(specifier) = Url::parse(current_specifier) {
let origin = base_url(&specifier);
let origin_len = origin.chars().count();
@@ -453,6 +567,7 @@ impl ModuleRegistry {
let path = &specifier[Position::BeforePath..];
let path_offset = offset - origin_len;
let mut completions = HashMap::<String, lsp::CompletionItem>::new();
+ let mut is_incomplete = false;
let mut did_match = false;
for registry in registries {
let tokens = parse(&registry.schema, None)
@@ -501,11 +616,26 @@ impl ModuleRegistry {
let maybe_url = registry.get_url_for_key(&key);
if let Some(url) = maybe_url {
if let Some(items) = self
- .get_variable_items(url, &tokens, &match_result)
+ .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 label = if let Some(p) = &prefix {
format!("{}{}", p, item)
@@ -546,6 +676,10 @@ impl ModuleRegistry {
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(registry, &specifier, &key, &item);
completions.insert(
item,
lsp::CompletionItem {
@@ -556,6 +690,8 @@ impl ModuleRegistry {
filter_text,
text_edit,
command,
+ preselect,
+ data,
..Default::default()
},
);
@@ -595,6 +731,7 @@ impl ModuleRegistry {
filter_text,
sort_text: Some("1".to_string()),
text_edit,
+ preselect: Some(true),
..Default::default()
},
);
@@ -609,6 +746,17 @@ impl ModuleRegistry {
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);
@@ -634,6 +782,9 @@ impl ModuleRegistry {
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);
completions.insert(
item.clone(),
lsp::CompletionItem {
@@ -644,6 +795,8 @@ impl ModuleRegistry {
filter_text,
text_edit,
command,
+ preselect,
+ data,
..Default::default()
},
);
@@ -663,7 +816,10 @@ impl ModuleRegistry {
return if completions.is_empty() && !did_match {
None
} else {
- Some(completions.into_iter().map(|(_, i)| i).collect())
+ Some(lsp::CompletionList {
+ items: completions.into_iter().map(|(_, i)| i).collect(),
+ is_incomplete,
+ })
};
}
}
@@ -672,11 +828,24 @@ impl ModuleRegistry {
self.get_origin_completions(current_specifier, range)
}
+ pub async fn get_documentation(
+ &self,
+ url: &str,
+ ) -> Option<lsp::Documentation> {
+ let specifier = Url::parse(url).ok()?;
+ let file = self
+ .file_fetcher
+ .fetch(&specifier, &mut Permissions::allow_all())
+ .await
+ .ok()?;
+ serde_json::from_str(&file.source).ok()
+ }
+
pub fn get_origin_completions(
&self,
current_specifier: &str,
range: &lsp::Range,
- ) -> Option<Vec<lsp::CompletionItem>> {
+ ) -> Option<lsp::CompletionList> {
let items = self
.origins
.keys()
@@ -704,13 +873,16 @@ impl ModuleRegistry {
})
.collect::<Vec<lsp::CompletionItem>>();
if !items.is_empty() {
- Some(items)
+ Some(lsp::CompletionList {
+ items,
+ is_incomplete: false,
+ })
} else {
None
}
}
- async fn get_items(&self, url: &str) -> Option<Vec<String>> {
+ async fn get_items(&self, url: &str) -> Option<VariableItems> {
let specifier = ModuleSpecifier::parse(url).ok()?;
let file = self
.file_fetcher
@@ -723,7 +895,7 @@ impl ModuleRegistry {
);
})
.ok()?;
- let items: Vec<String> = serde_json::from_str(&file.source)
+ let items: VariableItems = serde_json::from_str(&file.source)
.map_err(|err| {
error!(
"Error parsing response from endpoint \"{}\". {}",
@@ -736,15 +908,18 @@ impl ModuleRegistry {
async fn get_variable_items(
&self,
+ variable: &Key,
url: &str,
+ base: &Url,
tokens: &[Token],
match_result: &MatchResult,
- ) -> Option<Vec<String>> {
- let specifier = get_completion_endpoint(url, tokens, match_result)
- .map_err(|err| {
- error!("Internal error mapping endpoint \"{}\". {}", url, err);
- })
- .ok()?;
+ ) -> Option<VariableItems> {
+ let specifier =
+ get_endpoint_with_match(variable, url, base, tokens, match_result, None)
+ .map_err(|err| {
+ error!("Internal error mapping endpoint \"{}\". {}", url, err);
+ })
+ .ok()?;
let file = self
.file_fetcher
.fetch(&specifier, &mut Permissions::allow_all())
@@ -756,7 +931,7 @@ impl ModuleRegistry {
);
})
.ok()?;
- let items: Vec<String> = serde_json::from_str(&file.source)
+ let items: VariableItems = serde_json::from_str(&file.source)
.map_err(|err| {
error!(
"Error parsing response from endpoint \"{}\". {}",
@@ -776,7 +951,7 @@ mod tests {
#[test]
fn test_validate_registry_configuration() {
assert!(validate_config(&RegistryConfigurationJson {
- version: 2,
+ version: 3,
registries: vec![],
})
.is_err());
@@ -788,10 +963,12 @@ mod tests {
variables: vec![
RegistryConfigurationVariable {
key: "module".to_string(),
+ documentation: None,
url: "https://api.deno.land/modules?short".to_string(),
},
RegistryConfigurationVariable {
key: "version".to_string(),
+ documentation: None,
url: "https://deno.land/_vsc1/module/${module}".to_string(),
},
],
@@ -806,14 +983,17 @@ mod tests {
variables: vec![
RegistryConfigurationVariable {
key: "module".to_string(),
+ documentation: None,
url: "https://api.deno.land/modules?short".to_string(),
},
RegistryConfigurationVariable {
key: "version".to_string(),
+ documentation: None,
url: "https://deno.land/_vsc1/module/${module}/${path}".to_string(),
},
RegistryConfigurationVariable {
key: "path".to_string(),
+ documentation: None,
url: "https://deno.land/_vsc1/module/${module}/v/${{version}}"
.to_string(),
},
@@ -829,15 +1009,18 @@ mod tests {
variables: vec![
RegistryConfigurationVariable {
key: "module".to_string(),
+ documentation: None,
url: "https://api.deno.land/modules?short".to_string(),
},
RegistryConfigurationVariable {
key: "version".to_string(),
+ documentation: None,
url: "https://deno.land/_vsc1/module/${module}/v/${{version}}"
.to_string(),
},
RegistryConfigurationVariable {
key: "path".to_string(),
+ documentation: None,
url: "https://deno.land/_vsc1/module/${module}/v/${{version}}"
.to_string(),
},
@@ -853,21 +1036,66 @@ mod tests {
variables: vec![
RegistryConfigurationVariable {
key: "module".to_string(),
+ documentation: None,
url: "https://api.deno.land/modules?short".to_string(),
},
RegistryConfigurationVariable {
key: "version".to_string(),
+ documentation: None,
url: "https://deno.land/_vsc1/module/${module}".to_string(),
},
RegistryConfigurationVariable {
key: "path".to_string(),
+ documentation: None,
url: "https://deno.land/_vsc1/module/${module}/v/${{version}}"
.to_string(),
},
],
}],
};
- validate_config(&cfg).unwrap();
+ assert!(validate_config(&cfg).is_ok());
+
+ let cfg: RegistryConfigurationJson = serde_json::from_value(json!({
+ "version": 2,
+ "registries": [
+ {
+ "schema": "/x/:module([a-z0-9_]+)@:version?/:path",
+ "variables": [
+ {
+ "key": "module",
+ "documentation": "/api/details/mods/${module}",
+ "url": "/api/mods/${module}"
+ },
+ {
+ "key": "version",
+ "documentation": "/api/details/mods/${module}/v/${{version}}",
+ "url": "/api/mods/${module}/v/${{version}}"
+ },
+ {
+ "key": "path",
+ "documentation": "/api/details/mods/${module}/v/${{version}}/p/${path}",
+ "url": "/api/mods/${module}/v/${{version}}/p/${path}"
+ }
+ ]
+ },
+ {
+ "schema": "/x/:module([a-z0-9_]+)/:path",
+ "variables": [
+ {
+ "key": "module",
+ "documentation": "/api/details/mods/${module}",
+ "url": "/api/mods/${module}"
+ },
+ {
+ "key": "path",
+ "documentation": "/api/details/mods/${module}/v/latest/p/${path}",
+ "url": "/api/mods/${module}/v/latest/p/${path}"
+ }
+ ]
+ }
+ ]
+ })).unwrap();
+ assert!(validate_config(&cfg).is_ok());
}
#[tokio::test]
@@ -894,7 +1122,7 @@ mod tests {
.get_completions("h", 1, &range, |_| false)
.await;
assert!(completions.is_some());
- let completions = completions.unwrap();
+ let completions = completions.unwrap().items;
assert_eq!(completions.len(), 1);
assert_eq!(completions[0].label, "http://localhost:4545");
assert_eq!(
@@ -918,7 +1146,7 @@ mod tests {
.get_completions("http://localhost", 16, &range, |_| false)
.await;
assert!(completions.is_some());
- let completions = completions.unwrap();
+ let completions = completions.unwrap().items;
assert_eq!(completions.len(), 1);
assert_eq!(completions[0].label, "http://localhost:4545");
assert_eq!(
@@ -954,7 +1182,7 @@ mod tests {
.get_completions("http://localhost:4545", 21, &range, |_| false)
.await;
assert!(completions.is_some());
- let completions = completions.unwrap();
+ let completions = completions.unwrap().items;
assert_eq!(completions.len(), 1);
assert_eq!(completions[0].label, "/x");
assert_eq!(
@@ -978,7 +1206,7 @@ mod tests {
.get_completions("http://localhost:4545/", 22, &range, |_| false)
.await;
assert!(completions.is_some());
- let completions = completions.unwrap();
+ let completions = completions.unwrap().items;
assert_eq!(completions.len(), 1);
assert_eq!(completions[0].label, "/x");
assert_eq!(
@@ -1003,9 +1231,52 @@ mod tests {
.await;
assert!(completions.is_some());
let completions = completions.unwrap();
- assert_eq!(completions.len(), 2);
- assert!(completions[0].label == *"a" || completions[0].label == *"b");
- assert!(completions[1].label == *"a" || completions[1].label == *"b");
+ assert_eq!(completions.items.len(), 2);
+ assert!(completions.is_incomplete);
+ assert!(
+ completions.items[0].label == *"a" || completions.items[0].label == *"b"
+ );
+ assert!(
+ completions.items[1].label == *"a" || completions.items[1].label == *"b"
+ );
+
+ // testing for incremental searching for a module
+ let range = lsp::Range {
+ start: lsp::Position {
+ line: 0,
+ character: 20,
+ },
+ end: lsp::Position {
+ line: 0,
+ character: 45,
+ },
+ };
+ let completions = module_registry
+ .get_completions("http://localhost:4545/x/a", 25, &range, |_| false)
+ .await;
+ assert!(completions.is_some());
+ let completions = completions.unwrap();
+ assert_eq!(completions.items.len(), 4);
+ assert!(!completions.is_incomplete);
+ assert_eq!(
+ completions.items[0].data,
+ Some(json!({
+ "documentation": format!("http://localhost:4545/lsp/registries/doc_{}.json", completions.items[0].label),
+ }))
+ );
+
+ // testing getting the documentation
+ let documentation = module_registry
+ .get_documentation("http://localhost:4545/lsp/registries/doc_a.json")
+ .await;
+ assert_eq!(
+ documentation,
+ Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
+ kind: lsp::MarkupKind::Markdown,
+ value: "**a**".to_string(),
+ }))
+ );
+
let range = lsp::Range {
start: lsp::Position {
line: 0,
@@ -1020,7 +1291,7 @@ mod tests {
.get_completions("http://localhost:4545/x/a@", 26, &range, |_| false)
.await;
assert!(completions.is_some());
- let completions = completions.unwrap();
+ let completions = completions.unwrap().items;
assert_eq!(completions.len(), 3);
let range = lsp::Range {
start: lsp::Position {
@@ -1038,7 +1309,7 @@ mod tests {
})
.await;
assert!(completions.is_some());
- let completions = completions.unwrap();
+ let completions = completions.unwrap().items;
assert_eq!(completions.len(), 2);
assert_eq!(completions[0].detail, Some("(path)".to_string()));
assert_eq!(completions[0].kind, Some(lsp::CompletionItemKind::FILE));
@@ -1072,7 +1343,7 @@ mod tests {
.get_completions("http://localhost:4545/", 22, &range, |_| false)
.await;
assert!(completions.is_some());
- let completions = completions.unwrap();
+ let completions = completions.unwrap().items;
assert_eq!(completions.len(), 3);
for completion in completions {
assert!(completion.text_edit.is_some());
@@ -1101,7 +1372,7 @@ mod tests {
.get_completions("http://localhost:4545/cde@", 26, &range, |_| false)
.await;
assert!(completions.is_some());
- let completions = completions.unwrap();
+ let completions = completions.unwrap().items;
assert_eq!(completions.len(), 2);
for completion in completions {
assert!(completion.text_edit.is_some());
@@ -1141,7 +1412,7 @@ mod tests {
.get_completions("http://localhost:4545/", 22, &range, |_| false)
.await;
assert!(completions.is_some());
- let completions = completions.unwrap();
+ let completions = completions.unwrap().items;
assert_eq!(completions.len(), 3);
for completion in completions {
assert!(completion.text_edit.is_some());