diff options
Diffstat (limited to 'cli/lsp/registries.rs')
-rw-r--r-- | cli/lsp/registries.rs | 351 |
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(®istry.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()); |