diff options
Diffstat (limited to 'cli')
-rw-r--r-- | cli/lsp/README.md | 125 | ||||
-rw-r--r-- | cli/lsp/completions.rs | 120 | ||||
-rw-r--r-- | cli/lsp/config.rs | 60 | ||||
-rw-r--r-- | cli/lsp/language_server.rs | 104 | ||||
-rw-r--r-- | cli/lsp/lsp_custom.rs | 42 | ||||
-rw-r--r-- | cli/lsp/mod.rs | 1 | ||||
-rw-r--r-- | cli/lsp/registries.rs | 11 | ||||
-rw-r--r-- | cli/tests/integration_tests_lsp.rs | 47 |
8 files changed, 355 insertions, 155 deletions
diff --git a/cli/lsp/README.md b/cli/lsp/README.md index d480e3133..28ae50c6a 100644 --- a/cli/lsp/README.md +++ b/cli/lsp/README.md @@ -5,9 +5,8 @@ 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 highly experimental and far from feature -> complete. This document gives an overview of the structure of the language -> server. +> :warning: The Language Server is experimental and not feature complete. This +> document gives an overview of the structure of the language server. ## Structure @@ -15,6 +14,60 @@ 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.config` +- `deno.importMap` +- `deno.codeLens.implementations` +- `deno.codeLens.references` +- `deno.codeLens.referencesAllFunctions` +- `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 support on a per resource basis by the language +server: + +- `deno.enable` + +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. + ## Custom requests The LSP currently supports the following custom requests. A client should @@ -62,55 +115,27 @@ with Deno: } ``` -## Settings - -There are several settings that the language server supports for a workspace: - -- `deno.enable` -- `deno.config` -- `deno.import_map` -- `deno.code_lens.implementations` -- `deno.code_lens.references` -- `deno.code_lens.references_all_functions` -- `deno.suggest.complete_function_calls` -- `deno.suggest.names` -- `deno.suggest.paths` -- `deno.suggest.auto_imports` -- `deno.imports.hosts` -- `deno.lint` -- `deno.unstable` - -There are settings that are support on a per resource basis by the language -server: - -- `deno.enable` +## Custom notifications -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 -} -``` +There is currently one custom notification that is send from the server to the +client: -Would enable Deno with the unstable APIs for this instance of the language -server. +- `deno/registryStatus` - 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 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. + 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. -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. + The params for the notification are: -If the client does not have the `workspaceConfiguration` capability, the -language server will assume the workspace setting applies to all resources. + ```ts + interface RegistryStatusNotificationParams { + origin: string; + suggestions: boolean; + } + ``` diff --git a/cli/lsp/completions.rs b/cli/lsp/completions.rs index 56abeb9d2..95a13d59a 100644 --- a/cli/lsp/completions.rs +++ b/cli/lsp/completions.rs @@ -2,6 +2,7 @@ use super::analysis; use super::language_server; +use super::lsp_custom; use super::tsc; use crate::fs_util::is_supported_ext; @@ -9,6 +10,7 @@ use crate::media_type::MediaType; use deno_core::normalize_path; use deno_core::resolve_path; +use deno_core::resolve_url; use deno_core::serde::Deserialize; use deno_core::serde::Serialize; use deno_core::url::Position; @@ -34,6 +36,64 @@ pub struct CompletionItemData { pub tsc: Option<tsc::CompletionItemData>, } +/// Check if the origin can be auto-configured for completions, and if so, send +/// a notification to the client. +async fn check_auto_config_registry( + url_str: &str, + snapshot: &language_server::StateSnapshot, + client: lspower::Client, +) { + // check to see if auto discovery is enabled + if snapshot + .config + .settings + .workspace + .suggest + .imports + .auto_discover + { + if let Ok(specifier) = resolve_url(url_str) { + let scheme = specifier.scheme(); + let path = &specifier[Position::BeforePath..]; + if scheme.starts_with("http") + && !path.is_empty() + && url_str.ends_with(path) + { + // check to see if this origin is already explicitly set + let in_config = snapshot + .config + .settings + .workspace + .suggest + .imports + .hosts + .iter() + .any(|(h, _)| { + resolve_url(h).map(|u| u.origin()) == Ok(specifier.origin()) + }); + // if it isn't in the configuration, we will check to see if it supports + // suggestions and send a notification to the client. + if !in_config { + let origin = specifier.origin().ascii_serialization(); + let suggestions = snapshot + .module_registries + .fetch_config(&origin) + .await + .is_ok(); + client + .send_custom_notification::<lsp_custom::RegistryStateNotification>( + lsp_custom::RegistryStateNotificationParams { + origin, + suggestions, + }, + ) + .await; + } + } + } + } +} + /// Given a specifier, a position, and a snapshot, optionally return a /// completion response, which will be valid import completions for the specific /// context. @@ -41,6 +101,7 @@ pub async fn get_import_completions( specifier: &ModuleSpecifier, position: &lsp::Position, state_snapshot: &language_server::StateSnapshot, + client: lspower::Client, ) -> Option<lsp::CompletionResponse> { if let Ok(Some(source)) = state_snapshot.documents.content(specifier) { let media_type = MediaType::from(specifier); @@ -58,6 +119,8 @@ pub async fn get_import_completions( } // completion of modules from a module registry or cache if !current_specifier.is_empty() { + check_auto_config_registry(¤t_specifier, state_snapshot, client) + .await; let offset = if position.character > range.start.character { (position.character - range.start.character) as usize } else { @@ -808,11 +871,17 @@ mod tests { } #[tokio::test] - async fn test_get_import_completions() { + async fn test_get_workspace_completions() { let specifier = resolve_url("file:///a/b/c.ts").unwrap(); - let position = lsp::Position { - line: 0, - character: 21, + let range = lsp::Range { + start: lsp::Position { + line: 0, + character: 20, + }, + end: lsp::Position { + line: 0, + character: 21, + }, }; let state_snapshot = setup( &[ @@ -822,32 +891,29 @@ mod tests { &[("https://deno.land/x/a/b/c.ts", "console.log(1);\n")], ); let actual = - get_import_completions(&specifier, &position, &state_snapshot).await; + get_workspace_completions(&specifier, "h", &range, &state_snapshot); assert_eq!( actual, - Some(lsp::CompletionResponse::List(lsp::CompletionList { - is_incomplete: false, - items: vec![lsp::CompletionItem { - label: "https://deno.land/x/a/b/c.ts".to_string(), - kind: Some(lsp::CompletionItemKind::File), - detail: Some("(remote)".to_string()), - sort_text: Some("1".to_string()), - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range { - start: lsp::Position { - line: 0, - character: 20 - }, - end: lsp::Position { - line: 0, - character: 21, - } + vec![lsp::CompletionItem { + label: "https://deno.land/x/a/b/c.ts".to_string(), + kind: Some(lsp::CompletionItemKind::File), + detail: Some("(remote)".to_string()), + sort_text: Some("1".to_string()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 20 }, - new_text: "https://deno.land/x/a/b/c.ts".to_string(), - })), - ..Default::default() - }] - })) + end: lsp::Position { + line: 0, + character: 21, + } + }, + new_text: "https://deno.land/x/a/b/c.ts".to_string(), + })), + ..Default::default() + }] ); } } diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index 33f9122da..e46e8dd52 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -28,7 +28,7 @@ pub struct ClientCapabilities { pub line_folding_only: bool, } -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct CodeLensSettings { /// Flag for providing implementation code lenses. @@ -53,16 +53,20 @@ impl Default for CodeLensSettings { } } -#[derive(Debug, Clone, Deserialize)] +fn is_true() -> bool { + true +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct CompletionSettings { #[serde(default)] pub complete_function_calls: bool, - #[serde(default)] + #[serde(default = "is_true")] pub names: bool, - #[serde(default)] + #[serde(default = "is_true")] pub paths: bool, - #[serde(default)] + #[serde(default = "is_true")] pub auto_imports: bool, #[serde(default)] pub imports: ImportCompletionSettings, @@ -80,9 +84,15 @@ impl Default for CompletionSettings { } } -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ImportCompletionSettings { + /// A flag that indicates if non-explicitly set origins should be checked for + /// supporting import suggestions. + #[serde(default = "is_true")] + pub auto_discover: bool, + /// A map of origins which have had explicitly set if import suggestions are + /// enabled. #[serde(default)] pub hosts: HashMap<String, bool>, } @@ -90,6 +100,7 @@ pub struct ImportCompletionSettings { impl Default for ImportCompletionSettings { fn default() -> Self { Self { + auto_discover: true, hosts: HashMap::default(), } } @@ -104,10 +115,11 @@ pub struct SpecifierSettings { } /// Deno language server specific settings that are applied to a workspace. -#[derive(Debug, Default, Clone, Deserialize)] +#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct WorkspaceSettings { /// A flag that indicates if Deno is enabled for the workspace. + #[serde(default)] pub enable: bool, /// An option that points to a path string of the config file to apply to @@ -420,4 +432,38 @@ mod tests { .expect("could not update"); assert!(config.specifier_enabled(&specifier)); } + + #[test] + fn test_set_workspace_settings_defaults() { + let config = setup(); + config + .set_workspace_settings(json!({})) + .expect("could not update"); + assert_eq!( + config.get_workspace_settings(), + WorkspaceSettings { + enable: false, + config: None, + import_map: None, + code_lens: CodeLensSettings { + implementations: false, + references: false, + references_all_functions: false, + }, + internal_debug: false, + lint: false, + suggest: CompletionSettings { + complete_function_calls: false, + names: true, + paths: true, + auto_imports: true, + imports: ImportCompletionSettings { + auto_discover: true, + hosts: HashMap::new(), + } + }, + unstable: false, + } + ); + } } diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index c8b959596..3a751e319 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -3,8 +3,6 @@ use deno_core::error::anyhow; use deno_core::error::AnyError; use deno_core::resolve_url; -use deno_core::serde::Deserialize; -use deno_core::serde::Serialize; use deno_core::serde_json; use deno_core::serde_json::json; use deno_core::serde_json::Value; @@ -44,6 +42,7 @@ use super::config::SETTINGS_SECTION; use super::diagnostics; use super::diagnostics::DiagnosticSource; use super::documents::DocumentCache; +use super::lsp_custom; use super::performance::Performance; use super::registries; use super::sources; @@ -385,10 +384,9 @@ impl Inner { .iter() { if *enabled { - info!("Enabling auto complete registry for: {}", registry); + info!("Enabling import suggestions for: {}", registry); self.module_registries.enable(registry).await?; } else { - info!("Disabling auto complete registry for: {}", registry); self.module_registries.disable(registry).await?; } } @@ -552,17 +550,11 @@ impl Inner { async fn initialized(&mut self, _: InitializedParams) { // Check to see if we need to setup the import map if let Err(err) = self.update_import_map().await { - self - .client - .show_message(MessageType::Warning, err.to_string()) - .await; + self.client.show_message(MessageType::Warning, err).await; } // Check to see if we need to setup any module registries if let Err(err) = self.update_registries().await { - self - .client - .show_message(MessageType::Warning, err.to_string()) - .await; + self.client.show_message(MessageType::Warning, err).await; } if self @@ -713,22 +705,13 @@ impl Inner { self.update_debug_flag(); if let Err(err) = self.update_import_map().await { - self - .client - .show_message(MessageType::Warning, err.to_string()) - .await; + self.client.show_message(MessageType::Warning, err).await; } if let Err(err) = self.update_registries().await { - self - .client - .show_message(MessageType::Warning, err.to_string()) - .await; + self.client.show_message(MessageType::Warning, err).await; } if let Err(err) = self.update_tsconfig().await { - self - .client - .show_message(MessageType::Warning, err.to_string()) - .await; + self.client.show_message(MessageType::Warning, err).await; } if let Err(err) = self.diagnostics_server.update() { error!("{}", err); @@ -748,10 +731,7 @@ impl Inner { if let Some(import_map_uri) = &self.maybe_import_map_uri { if params.changes.iter().any(|fe| *import_map_uri == fe.uri) { if let Err(err) = self.update_import_map().await { - self - .client - .show_message(MessageType::Warning, err.to_string()) - .await; + self.client.show_message(MessageType::Warning, err).await; } } } @@ -759,10 +739,7 @@ impl Inner { if let Some(config_uri) = &self.maybe_config_uri { if params.changes.iter().any(|fe| *config_uri == fe.uri) { if let Err(err) = self.update_tsconfig().await { - self - .client - .show_message(MessageType::Warning, err.to_string()) - .await; + self.client.show_message(MessageType::Warning, err).await; } } } @@ -1549,6 +1526,7 @@ impl Inner { &specifier, ¶ms.text_document_position.position, &self.snapshot()?, + self.client.clone(), ) .await { @@ -2004,27 +1982,31 @@ impl Inner { params: Option<Value>, ) -> LspResult<Option<Value>> { match method { - "deno/cache" => match params.map(serde_json::from_value) { + lsp_custom::CACHE_REQUEST => match params.map(serde_json::from_value) { Some(Ok(params)) => self.cache(params).await, Some(Err(err)) => Err(LspError::invalid_params(err.to_string())), None => Err(LspError::invalid_params("Missing parameters")), }, - "deno/performance" => Ok(Some(self.get_performance())), - "deno/reloadImportRegistries" => self.reload_import_registries().await, - "deno/virtualTextDocument" => match params.map(serde_json::from_value) { - Some(Ok(params)) => Ok(Some( - serde_json::to_value(self.virtual_text_document(params).await?) - .map_err(|err| { - error!( - "Failed to serialize virtual_text_document response: {}", - err - ); - LspError::internal_error() - })?, - )), - Some(Err(err)) => Err(LspError::invalid_params(err.to_string())), - None => Err(LspError::invalid_params("Missing parameters")), - }, + lsp_custom::PERFORMANCE_REQUEST => Ok(Some(self.get_performance())), + lsp_custom::RELOAD_IMPORT_REGISTRIES_REQUEST => { + self.reload_import_registries().await + } + lsp_custom::VIRTUAL_TEXT_DOCUMENT => { + match params.map(serde_json::from_value) { + Some(Ok(params)) => Ok(Some( + serde_json::to_value(self.virtual_text_document(params).await?) + .map_err(|err| { + error!( + "Failed to serialize virtual_text_document response: {}", + err + ); + LspError::internal_error() + })?, + )), + Some(Err(err)) => Err(LspError::invalid_params(err.to_string())), + None => Err(LspError::invalid_params("Missing parameters")), + } + } _ => { error!("Got a {} request, but no handler is defined", method); Err(LspError::method_not_found()) @@ -2437,28 +2419,14 @@ impl lspower::LanguageServer for LanguageServer { } } -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -struct CacheParams { - /// The document currently open in the editor. If there are no `uris` - /// supplied, the referrer will be cached. - referrer: TextDocumentIdentifier, - /// Any documents that have been specifically asked to be cached via the - /// command. - uris: Vec<TextDocumentIdentifier>, -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -struct VirtualTextDocumentParams { - text_document: TextDocumentIdentifier, -} - // These are implementations of custom commands supported by the LSP impl Inner { /// Similar to `deno cache` on the command line, where modules will be cached /// in the Deno cache, including any of their dependencies. - async fn cache(&mut self, params: CacheParams) -> LspResult<Option<Value>> { + async fn cache( + &mut self, + params: lsp_custom::CacheParams, + ) -> LspResult<Option<Value>> { let mark = self.performance.mark("cache", Some(¶ms)); let referrer = self.url_map.normalize_url(¶ms.referrer.uri); if !params.uris.is_empty() { @@ -2519,7 +2487,7 @@ impl Inner { async fn virtual_text_document( &mut self, - params: VirtualTextDocumentParams, + params: lsp_custom::VirtualTextDocumentParams, ) -> LspResult<Option<String>> { let mark = self .performance diff --git a/cli/lsp/lsp_custom.rs b/cli/lsp/lsp_custom.rs new file mode 100644 index 000000000..c543a6c1b --- /dev/null +++ b/cli/lsp/lsp_custom.rs @@ -0,0 +1,42 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +use deno_core::serde::Deserialize; +use deno_core::serde::Serialize; +use lspower::lsp; + +pub const CACHE_REQUEST: &str = "deno/cache"; +pub const PERFORMANCE_REQUEST: &str = "deno/performance"; +pub const RELOAD_IMPORT_REGISTRIES_REQUEST: &str = + "deno/reloadImportRegistries"; +pub const VIRTUAL_TEXT_DOCUMENT: &str = "deno/virtualTextDocument"; + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CacheParams { + /// The document currently open in the editor. If there are no `uris` + /// supplied, the referrer will be cached. + pub referrer: lsp::TextDocumentIdentifier, + /// Any documents that have been specifically asked to be cached via the + /// command. + pub uris: Vec<lsp::TextDocumentIdentifier>, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct RegistryStateNotificationParams { + pub origin: String, + pub suggestions: bool, +} + +pub enum RegistryStateNotification {} + +impl lsp::notification::Notification for RegistryStateNotification { + type Params = RegistryStateNotificationParams; + + const METHOD: &'static str = "deno/registryState"; +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VirtualTextDocumentParams { + pub text_document: lsp::TextDocumentIdentifier, +} diff --git a/cli/lsp/mod.rs b/cli/lsp/mod.rs index 9d9c1ff86..488507a5b 100644 --- a/cli/lsp/mod.rs +++ b/cli/lsp/mod.rs @@ -11,6 +11,7 @@ mod config; mod diagnostics; mod documents; pub(crate) mod language_server; +mod lsp_custom; mod path_to_regex; mod performance; mod registries; diff --git a/cli/lsp/registries.rs b/cli/lsp/registries.rs index 877f03be3..29ec3258b 100644 --- a/cli/lsp/registries.rs +++ b/cli/lsp/registries.rs @@ -219,7 +219,7 @@ fn validate_config(config: &RegistryConfigurationJson) -> Result<(), AnyError> { } #[derive(Debug, Clone, Deserialize)] -struct RegistryConfigurationVariable { +pub(crate) struct RegistryConfigurationVariable { /// The name of the variable. key: String, /// The URL with variable substitutions of the endpoint that will provide @@ -228,7 +228,7 @@ struct RegistryConfigurationVariable { } #[derive(Debug, Clone, Deserialize)] -struct RegistryConfiguration { +pub(crate) struct RegistryConfiguration { /// A Express-like path which describes how URLs are composed for a registry. schema: String, /// The variables denoted in the `schema` should have a variable entry. @@ -339,7 +339,7 @@ impl ModuleRegistry { } /// Attempt to fetch the configuration for a specific origin. - async fn fetch_config( + pub(crate) async fn fetch_config( &self, origin: &str, ) -> Result<Vec<RegistryConfiguration>, AnyError> { @@ -443,6 +443,11 @@ impl ModuleRegistry { .await { let end = if p.is_some() { i + 1 } else { i }; + let end = if end > tokens.len() { + tokens.len() + } else { + end + }; let compiler = Compiler::new(&tokens[..end], None); for (idx, item) in items.into_iter().enumerate() { let label = if let Some(p) = &p { diff --git a/cli/tests/integration_tests_lsp.rs b/cli/tests/integration_tests_lsp.rs index 999d2de71..e4b963f2b 100644 --- a/cli/tests/integration_tests_lsp.rs +++ b/cli/tests/integration_tests_lsp.rs @@ -1840,6 +1840,53 @@ fn lsp_completions_registry_empty() { } #[test] +fn lsp_auto_discover_registry() { + let _g = http_server(); + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "import * as a from \"http://localhost:4545/x/a@\"" + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/completion", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 0, + "character": 46 + }, + "context": { + "triggerKind": 2, + "triggerCharacter": "@" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + let (method, maybe_res) = client.read_notification().unwrap(); + assert_eq!(method, "deno/registryState"); + assert_eq!( + maybe_res, + Some(json!({ + "origin": "http://localhost:4545", + "suggestions": true, + })) + ); + shutdown(&mut client); +} + +#[test] fn lsp_diagnostics_warn() { let _g = http_server(); let mut client = init("initialize_params.json"); |