diff options
Diffstat (limited to 'cli/lsp')
-rw-r--r-- | cli/lsp/README.md | 175 | ||||
-rw-r--r-- | cli/lsp/completions.rs | 25 | ||||
-rw-r--r-- | cli/lsp/language_server.rs | 6 | ||||
-rw-r--r-- | cli/lsp/registries.rs | 351 |
4 files changed, 335 insertions, 222 deletions
diff --git a/cli/lsp/README.md b/cli/lsp/README.md index 8b0135f25..d896b688b 100644 --- a/cli/lsp/README.md +++ b/cli/lsp/README.md @@ -5,176 +5,5 @@ The Deno Language Server provides a server implementation of the which is specifically tailored to provide a _Deno_ view of code. It is integrated into the command line and can be started via the `lsp` sub-command. -> :warning: The Language Server is experimental and not feature complete. This -> document gives an overview of the structure of the language server. - -## Structure - -When the language server is started, a `LanguageServer` instance is created -which holds all of the state of the language server. It also defines all of the -methods that the client calls via the Language Server RPC protocol. - -## Settings - -There are several settings that the language server supports for a workspace: - -- `deno.enable` -- `deno.cache` -- `deno.config` -- `deno.importMap` -- `deno.internalDebug` -- `deno.codeLens.implementations` -- `deno.codeLens.references` -- `deno.codeLens.referencesAllFunctions` -- `deno.codeLens.test` -- `deno.suggest.completeFunctionCalls` -- `deno.suggest.names` -- `deno.suggest.paths` -- `deno.suggest.autoImports` -- `deno.suggest.imports.autoDiscover` -- `deno.suggest.imports.hosts` -- `deno.lint` -- `deno.unstable` - -There are settings that are supported on a per resource basis by the language -server: - -- `deno.enable` -- `deno.codeLens.test` - -There are several points in the process where Deno analyzes these settings. -First, when the `initialize` request from the client, the -`initializationOptions` will be assumed to be an object that represents the -`deno` namespace of options. For example, the following value: - -```json -{ - "enable": true, - "unstable": true -} -``` - -Would enable Deno with the unstable APIs for this instance of the language -server. - -When the language server receives a `workspace/didChangeConfiguration` -notification, it will assess if the client has indicated if it has a -`workspaceConfiguration` capability. If it does, it will send a -`workspace/configuration` request which will include a request for the workspace -configuration as well as the configuration of all URIs that the language server -is currently tracking. - -If the client has the `workspaceConfiguration` capability, the language server -will send a configuration request for the URI when it received the -`textDocument/didOpen` notification in order to get the resources specific -settings. - -If the client does not have the `workspaceConfiguration` capability, the -language server will assume the workspace setting applies to all resources. - -## Commands - -There are several commands that might be issued by the language server to the -client, which the client is expected to implement: - -- `deno.cache` - This is sent as a resolution code action when there is an - un-cached module specifier that is being imported into a module. It will be - sent with and argument that contains the resolved specifier as a string to be - cached. -- `deno.showReferences` - This is sent as the command on some code lenses to - show locations of references. The arguments contain the specifier that is the - subject of the command, the start position of the target and the locations of - the references to show. -- `deno.test` - This is sent as part of a test code lens to, of which the client - is expected to run a test based on the arguments, which are the specifier the - test is contained in and the name of the test to filter the tests on. - -## Requests - -The LSP currently supports the following custom requests. A client should -implement these in order to have a fully functioning client that integrates well -with Deno: - -- `deno/cache` - This command will instruct Deno to attempt to cache a module - and all of its dependencies. If a `referrer` only is passed, then all - dependencies for the module specifier will be loaded. If there are values in - the `uris`, then only those `uris` will be cached. - - It expects parameters of: - - ```ts - interface CacheParams { - referrer: TextDocumentIdentifier; - uris: TextDocumentIdentifier[]; - } - ``` -- `deno/performance` - Requests the return of the timing averages for the - internal instrumentation of Deno. - - It does not expect any parameters. -- `deno/reloadImportRegistries` - Reloads any cached responses from import - registries. - - It does not expect any parameters. -- `deno/virtualTextDocument` - Requests a virtual text document from the LSP, - which is a read only document that can be displayed in the client. This allows - clients to access documents in the Deno cache, like remote modules and - TypeScript library files built into Deno. The Deno language server will encode - all internal files under the custom schema `deno:`, so clients should route - all requests for the `deno:` schema back to the `deno/virtualTextDocument` - API. - - It also supports a special URL of `deno:/status.md` which provides a markdown - formatted text document that contains details about the status of the LSP for - display to a user. - - It expects parameters of: - - ```ts - interface VirtualTextDocumentParams { - textDocument: TextDocumentIdentifier; - } - ``` - -## Notifications - -There is currently one custom notification that is sent from the server to the -client: - -- `deno/registryState` - when `deno.suggest.imports.autoDiscover` is `true` and - an origin for an import being added to a document is not explicitly set in - `deno.suggest.imports.hosts`, the origin will be checked and the notification - will be sent to the client of the status. - - When receiving the notification, if the param `suggestion` is `true`, the - client should offer the user the choice to enable the origin and add it to the - configuration for `deno.suggest.imports.hosts`. If `suggestion` is `false` the - client should add it to the configuration of as `false` to stop the language - server from attempting to detect if suggestions are supported. - - The params for the notification are: - - ```ts - interface RegistryStatusNotificationParams { - origin: string; - suggestions: boolean; - } - ``` - -## Language IDs - -The language server supports diagnostics and formatting for the following -[text document language IDs](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentItem): - -- `"javascript"` -- `"javascriptreact"` -- `"jsx"` _non standard, same as `javascriptreact`_ -- `"typescript"` -- `"typescriptreact"` -- `"tsx"` _non standard, same as `typescriptreact`_ - -The language server supports only formatting for the following language IDs: - -- `"json"` -- `"jsonc"` -- `"markdown"` +This documentation has been moved to the +[Deno manual](https://deno.land/manual/language_server/overview). diff --git a/cli/lsp/completions.rs b/cli/lsp/completions.rs index 601373145..50ab2660f 100644 --- a/cli/lsp/completions.rs +++ b/cli/lsp/completions.rs @@ -24,6 +24,8 @@ const LOCAL_PATHS: &[&str] = &[CURRENT_PATH, PARENT_PATH]; #[serde(rename_all = "camelCase")] pub struct CompletionItemData { #[serde(skip_serializing_if = "Option::is_none")] + pub documentation: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] pub tsc: Option<tsc::CompletionItemData>, } @@ -132,19 +134,22 @@ pub(crate) async fn get_import_completions( } else { 0 }; - let maybe_items = state_snapshot + let maybe_list = state_snapshot .module_registries .get_completions(&text, offset, &range, |specifier| { state_snapshot.documents.contains_specifier(specifier) }) .await; - let items = maybe_items.unwrap_or_else(|| { - get_workspace_completions(specifier, &text, &range, state_snapshot) - }); - Some(lsp::CompletionResponse::List(lsp::CompletionList { + let list = maybe_list.unwrap_or_else(|| lsp::CompletionList { + items: get_workspace_completions( + specifier, + &text, + &range, + state_snapshot, + ), is_incomplete: false, - items, - })) + }); + Some(lsp::CompletionResponse::List(list)) } else { let mut items: Vec<lsp::CompletionItem> = LOCAL_PATHS .iter() @@ -157,14 +162,16 @@ pub(crate) async fn get_import_completions( ..Default::default() }) .collect(); + let mut is_incomplete = false; if let Some(origin_items) = state_snapshot .module_registries .get_origin_completions(&text, &range) { - items.extend(origin_items); + is_incomplete = origin_items.is_incomplete; + items.extend(origin_items.items); } Some(lsp::CompletionResponse::List(lsp::CompletionList { - is_incomplete: false, + is_incomplete, items, })) // TODO(@kitsonk) add bare specifiers from import map diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 59755db8b..aa465cf48 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -1778,6 +1778,12 @@ impl Inner { ); params } + } else if let Some(url) = data.documentation { + CompletionItem { + documentation: self.module_registries.get_documentation(&url).await, + data: None, + ..params + } } else { params } 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()); |