diff --git a/cli/lsp/client.rs b/cli/lsp/client.rs index 210aa1da32..84953c4d45 100644 --- a/cli/lsp/client.rs +++ b/cli/lsp/client.rs @@ -6,14 +6,13 @@ use async_trait::async_trait; use deno_core::anyhow::anyhow; use deno_core::anyhow::bail; use deno_core::error::AnyError; -use deno_core::serde_json; +use deno_core::serde_json::json; use deno_core::unsync::spawn; use tower_lsp::lsp_types as lsp; use tower_lsp::lsp_types::ConfigurationItem; use crate::lsp::repl::get_repl_workspace_settings; -use super::config::SpecifierSettings; use super::config::WorkspaceSettings; use super::config::SETTINGS_SECTION; use super::lsp_custom; @@ -125,46 +124,11 @@ impl OutsideLockClient { self.0.register_capability(registrations).await } - pub async fn specifier_configurations( - &self, - specifiers: Vec, - ) -> Result>, AnyError> { - self - .0 - .specifier_configurations( - specifiers.into_iter().map(|s| s.into_url()).collect(), - ) - .await - } - - pub async fn specifier_configuration( - &self, - specifier: &LspClientUrl, - ) -> Result { - let values = self - .0 - .specifier_configurations(vec![specifier.as_url().clone()]) - .await?; - if let Some(value) = values.into_iter().next() { - value.map_err(|err| { - anyhow!( - "Error converting specifier settings ({}): {}", - specifier, - err - ) - }) - } else { - bail!( - "Expected the client to return a configuration item for specifier: {}", - specifier - ); - } - } - pub async fn workspace_configuration( &self, - ) -> Result { - self.0.workspace_configuration().await + scopes: Vec>, + ) -> Result, AnyError> { + self.0.workspace_configuration(scopes).await } pub async fn publish_diagnostics( @@ -201,13 +165,10 @@ trait ClientTrait: Send + Sync { &self, params: lsp_custom::DidChangeDenoConfigurationNotificationParams, ); - async fn specifier_configurations( - &self, - uris: Vec, - ) -> Result>, AnyError>; async fn workspace_configuration( &self, - ) -> Result; + scopes: Vec>, + ) -> Result, AnyError>; async fn show_message(&self, message_type: lsp::MessageType, text: String); async fn register_capability( &self, @@ -288,67 +249,50 @@ impl ClientTrait for TowerClient { .await } - async fn specifier_configurations( + async fn workspace_configuration( &self, - uris: Vec, - ) -> Result>, AnyError> { + scopes: Vec>, + ) -> Result, AnyError> { let config_response = self .0 .configuration( - uris - .into_iter() - .map(|uri| ConfigurationItem { - scope_uri: Some(uri), - section: Some(SETTINGS_SECTION.to_string()), + scopes + .iter() + .flat_map(|scope_uri| { + vec![ + ConfigurationItem { + scope_uri: scope_uri.clone(), + section: Some(SETTINGS_SECTION.to_string()), + }, + ConfigurationItem { + scope_uri: scope_uri.clone(), + section: Some("javascript".to_string()), + }, + ConfigurationItem { + scope_uri: scope_uri.clone(), + section: Some("typescript".to_string()), + }, + ] }) .collect(), ) - .await?; - - Ok( - config_response - .into_iter() - .map(|value| { - serde_json::from_value::(value).map_err(|err| { - anyhow!("Error converting specifier settings: {}", err) - }) - }) - .collect(), - ) - } - - async fn workspace_configuration( - &self, - ) -> Result { - let config_response = self - .0 - .configuration(vec![ - ConfigurationItem { - scope_uri: None, - section: Some(SETTINGS_SECTION.to_string()), - }, - ConfigurationItem { - scope_uri: None, - section: Some("javascript".to_string()), - }, - ConfigurationItem { - scope_uri: None, - section: Some("typescript".to_string()), - }, - ]) .await; match config_response { Ok(configs) => { let mut configs = configs.into_iter(); - let deno = serde_json::to_value(configs.next()).unwrap(); - let javascript = serde_json::to_value(configs.next()).unwrap(); - let typescript = serde_json::to_value(configs.next()).unwrap(); - Ok(WorkspaceSettings::from_raw_settings( - deno, javascript, typescript, - )) + let mut result = Vec::with_capacity(scopes.len()); + for _ in 0..scopes.len() { + let deno = json!(configs.next()); + let javascript = json!(configs.next()); + let typescript = json!(configs.next()); + result.push(WorkspaceSettings::from_raw_settings( + deno, javascript, typescript, + )); + } + Ok(result) } Err(err) => { - bail!("Error getting workspace configuration: {}", err) + bail!("Error getting workspace configurations: {}", err) } } } @@ -406,27 +350,11 @@ impl ClientTrait for ReplClient { ) { } - async fn specifier_configurations( - &self, - uris: Vec, - ) -> Result>, AnyError> { - // all specifiers are enabled for the REPL - let settings = uris - .into_iter() - .map(|_| { - Ok(SpecifierSettings { - enable: Some(true), - ..Default::default() - }) - }) - .collect(); - Ok(settings) - } - async fn workspace_configuration( &self, - ) -> Result { - Ok(get_repl_workspace_settings()) + scopes: Vec>, + ) -> Result, AnyError> { + Ok(vec![get_repl_workspace_settings(); scopes.len()]) } async fn show_message( diff --git a/cli/lsp/code_lens.rs b/cli/lsp/code_lens.rs index cab7af1a6b..d0885294fe 100644 --- a/cli/lsp/code_lens.rs +++ b/cli/lsp/code_lens.rs @@ -408,7 +408,7 @@ fn collect_test( config: &Config, ) -> Result, AnyError> { if config.specifier_enabled_for_test(specifier) - && config.specifier_code_lens_test(specifier) + && config.enabled_code_lens_test_for_specifier(specifier) { if let Some(parsed_source) = parsed_source { let mut collector = diff --git a/cli/lsp/completions.rs b/cli/lsp/completions.rs index 08f8dbf81e..2b528b010c 100644 --- a/cli/lsp/completions.rs +++ b/cli/lsp/completions.rs @@ -2,6 +2,7 @@ use super::client::Client; use super::config::ConfigSnapshot; +use super::config::WorkspaceSettings; use super::documents::file_like_to_file_specifier; use super::documents::Documents; use super::documents::DocumentsFilter; @@ -52,12 +53,12 @@ pub struct CompletionItemData { /// a notification to the client. async fn check_auto_config_registry( url_str: &str, - config: &ConfigSnapshot, + workspace_settings: &WorkspaceSettings, client: &Client, module_registries: &ModuleRegistry, ) { // check to see if auto discovery is enabled - if config.settings.workspace.suggest.imports.auto_discover { + if workspace_settings.suggest.imports.auto_discover { if let Ok(specifier) = resolve_url(url_str) { let scheme = specifier.scheme(); let path = &specifier[Position::BeforePath..]; @@ -67,11 +68,14 @@ async fn check_auto_config_registry( { // check to see if this origin is already explicitly set let in_config = - config.settings.workspace.suggest.imports.hosts.iter().any( - |(h, _)| { + workspace_settings + .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 { @@ -176,7 +180,13 @@ pub async fn get_import_completions( })) } else if !text.is_empty() { // completion of modules from a module registry or cache - check_auto_config_registry(&text, config, client, module_registries).await; + check_auto_config_registry( + &text, + config.workspace_settings_for_specifier(specifier), + client, + module_registries, + ) + .await; let offset = if position.character > range.start.character { (position.character - range.start.character) as usize } else { diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index 6a005e83b4..ebcedb0240 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -73,21 +73,6 @@ impl Default for CodeLensSettings { } } -#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct CodeLensSpecifierSettings { - /// Flag for providing test code lens on `Deno.test` statements. There is - /// also the `test_args` setting, but this is not used by the server. - #[serde(default = "is_true")] - pub test: bool, -} - -impl Default for CodeLensSpecifierSettings { - fn default() -> Self { - Self { test: true } - } -} - #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct DenoCompletionSettings { @@ -277,25 +262,6 @@ impl Default for ImportCompletionSettings { } } -/// Deno language server specific settings that can be applied uniquely to a -/// specifier. -#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct SpecifierSettings { - /// A flag that indicates if Deno is enabled for this specifier or not. - pub enable: Option, - /// A list of paths, using the workspace folder as a base that should be Deno - /// disabled. - #[serde(default)] - pub disable_paths: Vec, - /// A list of paths, using the workspace folder as a base that should be Deno - /// enabled. - pub enable_paths: Option>, - /// Code lens specific settings for the resource. - #[serde(default)] - pub code_lens: CodeLensSpecifierSettings, -} - #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct TestingSettings { @@ -712,56 +678,6 @@ impl WorkspaceSettings { .unwrap_or_default(); Self::from_raw_settings(deno, javascript, typescript) } - - /// Determine if any code lenses are enabled at all. This allows short - /// circuiting when there are no code lenses enabled. - pub fn enabled_code_lens(&self) -> bool { - self.code_lens.implementations || self.code_lens.references - } - - // TODO(nayeemrmn): Factor in out-of-band media type here. - pub fn language_settings_for_specifier( - &self, - specifier: &ModuleSpecifier, - ) -> Option<&LanguageWorkspaceSettings> { - if specifier.scheme() == "deno-notebook-cell" { - return Some(&self.typescript); - } - match MediaType::from_specifier(specifier) { - MediaType::JavaScript - | MediaType::Jsx - | MediaType::Mjs - | MediaType::Cjs => Some(&self.javascript), - MediaType::TypeScript - | MediaType::Mts - | MediaType::Cts - | MediaType::Dts - | MediaType::Dmts - | MediaType::Dcts - | MediaType::Tsx => Some(&self.typescript), - MediaType::Json - | MediaType::Wasm - | MediaType::TsBuildInfo - | MediaType::SourceMap - | MediaType::Unknown => None, - } - } - - /// Determine if any inlay hints are enabled. This allows short circuiting - /// when there are no inlay hints enabled. - pub fn enabled_inlay_hints(&self, specifier: &ModuleSpecifier) -> bool { - let Some(settings) = self.language_settings_for_specifier(specifier) else { - return false; - }; - !matches!( - settings.inlay_hints.parameter_names.enabled, - InlayHintsParamNamesEnabled::None - ) || settings.inlay_hints.parameter_types.enabled - || settings.inlay_hints.variable_types.enabled - || settings.inlay_hints.property_declaration_types.enabled - || settings.inlay_hints.function_like_return_types.enabled - || settings.inlay_hints.enum_member_values.enabled - } } #[derive(Debug, Clone, Default)] @@ -773,6 +689,13 @@ pub struct ConfigSnapshot { } impl ConfigSnapshot { + pub fn workspace_settings_for_specifier( + &self, + specifier: &ModuleSpecifier, + ) -> &WorkspaceSettings { + self.settings.get_for_specifier(specifier).0 + } + /// Determine if the provided specifier is enabled or not. pub fn specifier_enabled(&self, specifier: &ModuleSpecifier) -> bool { specifier_enabled( @@ -803,8 +726,59 @@ impl ConfigSnapshot { #[derive(Debug, Default, Clone)] pub struct Settings { - pub specifiers: BTreeMap, - pub workspace: WorkspaceSettings, + pub unscoped: WorkspaceSettings, + pub by_workspace_folder: Option>, +} + +impl Settings { + pub fn get_unscoped(&self) -> &WorkspaceSettings { + &self.unscoped + } + + pub fn get_for_specifier( + &self, + specifier: &ModuleSpecifier, + ) -> (&WorkspaceSettings, Option<&ModuleSpecifier>) { + let Ok(path) = specifier_to_file_path(specifier) else { + return (&self.unscoped, None); + }; + if let Some(by_workspace_folder) = &self.by_workspace_folder { + for (folder_uri, settings) in by_workspace_folder.iter().rev() { + let Ok(folder_path) = specifier_to_file_path(folder_uri) else { + continue; + }; + if path.starts_with(folder_path) { + return (settings, Some(folder_uri)); + } + } + } + (&self.unscoped, None) + } + + pub fn set_unscoped(&mut self, mut settings: WorkspaceSettings) { + // See https://github.com/denoland/vscode_deno/issues/908. + if settings.enable_paths == Some(vec![]) { + settings.enable_paths = None; + } + self.unscoped = settings; + } + + pub fn set_for_workspace_folders( + &mut self, + mut by_workspace_folder: Option< + BTreeMap, + >, + ) { + if let Some(by_workspace_folder) = &mut by_workspace_folder { + for settings in by_workspace_folder.values_mut() { + // See https://github.com/denoland/vscode_deno/issues/908. + if settings.enable_paths == Some(vec![]) { + settings.enable_paths = None; + } + } + } + self.by_workspace_folder = by_workspace_folder; + } } #[derive(Debug)] @@ -860,6 +834,93 @@ impl Config { config } + pub fn set_workspace_settings( + &mut self, + unscoped: WorkspaceSettings, + by_workspace_folder: Option>, + ) { + self.settings.set_unscoped(unscoped); + self.settings.set_for_workspace_folders(by_workspace_folder); + } + + pub fn workspace_settings(&self) -> &WorkspaceSettings { + self.settings.get_unscoped() + } + + pub fn workspace_settings_for_specifier( + &self, + specifier: &ModuleSpecifier, + ) -> &WorkspaceSettings { + self.settings.get_for_specifier(specifier).0 + } + + pub fn language_settings_for_specifier( + &self, + specifier: &ModuleSpecifier, + ) -> Option<&LanguageWorkspaceSettings> { + let workspace_settings = self.workspace_settings_for_specifier(specifier); + if specifier.scheme() == "deno-notebook-cell" { + return Some(&workspace_settings.typescript); + } + match MediaType::from_specifier(specifier) { + MediaType::JavaScript + | MediaType::Jsx + | MediaType::Mjs + | MediaType::Cjs => Some(&workspace_settings.javascript), + MediaType::TypeScript + | MediaType::Mts + | MediaType::Cts + | MediaType::Dts + | MediaType::Dmts + | MediaType::Dcts + | MediaType::Tsx => Some(&workspace_settings.typescript), + MediaType::Json + | MediaType::Wasm + | MediaType::TsBuildInfo + | MediaType::SourceMap + | MediaType::Unknown => None, + } + } + + /// Determine if any inlay hints are enabled. This allows short circuiting + /// when there are no inlay hints enabled. + pub fn enabled_inlay_hints_for_specifier( + &self, + specifier: &ModuleSpecifier, + ) -> bool { + let Some(settings) = self.language_settings_for_specifier(specifier) else { + return false; + }; + !matches!( + settings.inlay_hints.parameter_names.enabled, + InlayHintsParamNamesEnabled::None + ) || settings.inlay_hints.parameter_types.enabled + || settings.inlay_hints.variable_types.enabled + || settings.inlay_hints.property_declaration_types.enabled + || settings.inlay_hints.function_like_return_types.enabled + || settings.inlay_hints.enum_member_values.enabled + } + + /// Determine if any code lenses are enabled at all. This allows short + /// circuiting when there are no code lenses enabled. + pub fn enabled_code_lens_for_specifier( + &self, + specifier: &ModuleSpecifier, + ) -> bool { + let settings = self.workspace_settings_for_specifier(specifier); + settings.code_lens.implementations + || settings.code_lens.references + || settings.code_lens.test + } + + pub fn enabled_code_lens_test_for_specifier( + &self, + specifier: &ModuleSpecifier, + ) -> bool { + let settings = self.workspace_settings_for_specifier(specifier); + settings.code_lens.test + } + pub fn root_uri(&self) -> Option<&Url> { self.workspace_folders.get(0).map(|p| &p.0) } @@ -949,20 +1010,6 @@ impl Config { }); } - pub fn workspace_settings(&self) -> &WorkspaceSettings { - &self.settings.workspace - } - - /// Set the workspace settings directly, which occurs during initialization - /// and when the client does not support workspace configuration requests - pub fn set_workspace_settings(&mut self, settings: WorkspaceSettings) { - self.settings.workspace = settings; - // See https://github.com/denoland/vscode_deno/issues/908. - if self.settings.workspace.enable_paths == Some(vec![]) { - self.settings.workspace.enable_paths = None; - } - } - pub fn snapshot(&self) -> Arc { Arc::new(ConfigSnapshot { client_capabilities: self.client_capabilities.clone(), @@ -972,10 +1019,6 @@ impl Config { }) } - pub fn has_specifier_settings(&self, specifier: &ModuleSpecifier) -> bool { - self.settings.specifiers.contains_key(specifier) - } - pub fn specifier_enabled(&self, specifier: &ModuleSpecifier) -> bool { specifier_enabled( specifier, @@ -1009,11 +1052,8 @@ impl Config { lsp_log!("Unable to convert uri \"{}\" to path.", workspace_uri); continue; }; - let specifier_settings = self.settings.specifiers.get(workspace_uri); - let enable_paths = specifier_settings - .and_then(|s| s.enable_paths.as_ref()) - .or(self.settings.workspace.enable_paths.as_ref()); - if let Some(enable_paths) = enable_paths { + let settings = self.workspace_settings_for_specifier(workspace_uri); + if let Some(enable_paths) = &settings.enable_paths { for path in enable_paths { paths.push(workspace_path.join(path)); } @@ -1035,25 +1075,14 @@ impl Config { } } } - let root_enable = self - .settings - .workspace - .enable - .unwrap_or(self.has_config_file()); for (workspace_uri, _) in &self.workspace_folders { let Ok(workspace_path) = specifier_to_file_path(workspace_uri) else { lsp_log!("Unable to convert uri \"{}\" to path.", workspace_uri); continue; }; - let specifier_settings = self.settings.specifiers.get(workspace_uri); - let enable = specifier_settings - .and_then(|s| s.enable) - .unwrap_or(root_enable); - if enable { - let disable_paths = specifier_settings - .map(|s| &s.disable_paths) - .unwrap_or(&self.settings.workspace.disable_paths); - for path in disable_paths { + let settings = self.workspace_settings_for_specifier(workspace_uri); + if settings.enable.unwrap_or_else(|| self.has_config_file()) { + for path in &settings.disable_paths { paths.push(workspace_path.join(path)); } } else { @@ -1065,16 +1094,6 @@ impl Config { paths } - pub fn specifier_code_lens_test(&self, specifier: &ModuleSpecifier) -> bool { - let value = self - .settings - .specifiers - .get(specifier) - .map(|settings| settings.code_lens.test) - .unwrap_or_else(|| self.settings.workspace.code_lens.test); - value - } - pub fn update_capabilities( &mut self, capabilities: &lsp::ClientCapabilities, @@ -1127,37 +1146,13 @@ impl Config { }; } } - - pub fn get_specifiers(&self) -> Vec { - self.settings.specifiers.keys().cloned().collect() - } - - pub fn set_specifier_settings( - &mut self, - specifier: ModuleSpecifier, - mut settings: SpecifierSettings, - ) -> bool { - // See https://github.com/denoland/vscode_deno/issues/908. - if settings.enable_paths == Some(vec![]) { - settings.enable_paths = None; - } - - if let Some(existing) = self.settings.specifiers.get(&specifier) { - if *existing == settings { - return false; - } - } - - self.settings.specifiers.insert(specifier, settings); - true - } } fn specifier_enabled( specifier: &Url, config_file: Option<&ConfigFile>, settings: &Settings, - workspace_folders: &Vec<(Url, lsp::WorkspaceFolder)>, + workspace_folders: &[(Url, lsp::WorkspaceFolder)], ) -> bool { if let Some(cf) = config_file { if let Some(files) = cf.to_files_config().ok().flatten() { @@ -1166,55 +1161,42 @@ fn specifier_enabled( } } } - - let root_enable = settings.workspace.enable.unwrap_or(config_file.is_some()); - if let Some(settings) = settings.specifiers.get(specifier) { - // TODO(nayeemrmn): We don't know from where to resolve path lists in this - // case. If they're detected, instead defer to workspace scopes. - if settings.enable_paths.is_none() && settings.disable_paths.is_empty() { - return settings.enable.unwrap_or(root_enable); - } - } let Ok(path) = specifier_to_file_path(specifier) else { // Non-file URLs are not disabled by these settings. return true; }; - for (workspace_uri, _) in workspace_folders { - let Ok(workspace_path) = specifier_to_file_path(workspace_uri) else { - lsp_log!("Unable to convert uri \"{}\" to path.", workspace_uri); - continue; - }; - if path.starts_with(&workspace_path) { - let specifier_settings = settings.specifiers.get(workspace_uri); - let disable_paths = specifier_settings - .map(|s| &s.disable_paths) - .unwrap_or(&settings.workspace.disable_paths); - let resolved_disable_paths = disable_paths + let (settings, mut folder_uri) = settings.get_for_specifier(specifier); + folder_uri = folder_uri.or_else(|| workspace_folders.get(0).map(|f| &f.0)); + let mut disable_paths = vec![]; + let mut enable_paths = None; + if let Some(folder_uri) = folder_uri { + if let Ok(folder_path) = specifier_to_file_path(folder_uri) { + disable_paths = settings + .disable_paths .iter() - .map(|p| workspace_path.join(p)) + .map(|p| folder_path.join(p)) .collect::>(); - let enable_paths = specifier_settings - .and_then(|s| s.enable_paths.as_ref()) - .or(settings.workspace.enable_paths.as_ref()); - if let Some(enable_paths) = enable_paths { - for enable_path in enable_paths { - let enable_path = workspace_path.join(enable_path); - if path.starts_with(&enable_path) - && !resolved_disable_paths.iter().any(|p| path.starts_with(p)) - { - return true; - } - } - return false; - } else { - return specifier_settings - .and_then(|s| s.enable) - .unwrap_or(root_enable) - && !resolved_disable_paths.iter().any(|p| path.starts_with(p)); - } + enable_paths = settings.enable_paths.as_ref().map(|enable_paths| { + enable_paths + .iter() + .map(|p| folder_path.join(p)) + .collect::>() + }); } } - root_enable + if let Some(enable_paths) = &enable_paths { + for enable_path in enable_paths { + if path.starts_with(enable_path) + && !disable_paths.iter().any(|p| path.starts_with(p)) + { + return true; + } + } + false + } else { + settings.enable.unwrap_or_else(|| config_file.is_some()) + && !disable_paths.iter().any(|p| path.starts_with(p)) + } } fn resolve_lockfile_from_config(config_file: &ConfigFile) -> Option { @@ -1285,6 +1267,7 @@ mod tests { "enable": true })) .unwrap(), + None, ); assert!(config.specifier_enabled(&specifier)); } @@ -1300,6 +1283,7 @@ mod tests { "enable": true })) .unwrap(), + None, ); let config_snapshot = config.snapshot(); assert!(config_snapshot.specifier_enabled(&specifier)); @@ -1315,7 +1299,7 @@ mod tests { assert!(!config.specifier_enabled(&specifier_b)); let workspace_settings = serde_json::from_str(r#"{ "enablePaths": ["worker"] }"#).unwrap(); - config.set_workspace_settings(workspace_settings); + config.set_workspace_settings(workspace_settings, None); assert!(config.specifier_enabled(&specifier_a)); assert!(!config.specifier_enabled(&specifier_b)); let config_snapshot = config.snapshot(); @@ -1327,10 +1311,10 @@ mod tests { fn test_config_specifier_disabled_path() { let root_uri = resolve_url("file:///root/").unwrap(); let mut config = Config::new_with_root(root_uri.clone()); - config.settings.workspace.enable = Some(true); - config.settings.workspace.enable_paths = + config.settings.unscoped.enable = Some(true); + config.settings.unscoped.enable_paths = Some(vec!["mod1.ts".to_string(), "mod2.ts".to_string()]); - config.settings.workspace.disable_paths = vec!["mod2.ts".to_string()]; + config.settings.unscoped.disable_paths = vec!["mod2.ts".to_string()]; assert!(config.specifier_enabled(&root_uri.join("mod1.ts").unwrap())); assert!(!config.specifier_enabled(&root_uri.join("mod2.ts").unwrap())); @@ -1340,7 +1324,8 @@ mod tests { #[test] fn test_set_workspace_settings_defaults() { let mut config = Config::new(); - config.set_workspace_settings(serde_json::from_value(json!({})).unwrap()); + config + .set_workspace_settings(serde_json::from_value(json!({})).unwrap(), None); assert_eq!( config.workspace_settings().clone(), WorkspaceSettings { @@ -1472,6 +1457,7 @@ mod tests { let mut config = Config::new(); config.set_workspace_settings( serde_json::from_value(json!({ "cache": "" })).unwrap(), + None, ); assert_eq!( config.workspace_settings().clone(), @@ -1484,6 +1470,7 @@ mod tests { let mut config = Config::new(); config.set_workspace_settings( serde_json::from_value(json!({ "import_map": "" })).unwrap(), + None, ); assert_eq!( config.workspace_settings().clone(), @@ -1496,6 +1483,7 @@ mod tests { let mut config = Config::new(); config.set_workspace_settings( serde_json::from_value(json!({ "tls_certificate": "" })).unwrap(), + None, ); assert_eq!( config.workspace_settings().clone(), @@ -1508,6 +1496,7 @@ mod tests { let mut config = Config::new(); config.set_workspace_settings( serde_json::from_value(json!({ "config": "" })).unwrap(), + None, ); assert_eq!( config.workspace_settings().clone(), @@ -1541,30 +1530,39 @@ mod tests { }, ), ]; - config.set_specifier_settings( - Url::parse("file:///root1/").unwrap(), - SpecifierSettings { - enable_paths: Some(vec![ - "sub_dir".to_string(), - "sub_dir/other".to_string(), - "test.ts".to_string(), - ]), - ..Default::default() - }, - ); - config.set_specifier_settings( - Url::parse("file:///root2/").unwrap(), - SpecifierSettings { - enable_paths: Some(vec!["other.ts".to_string()]), - ..Default::default() - }, - ); - config.set_specifier_settings( - Url::parse("file:///root3/").unwrap(), - SpecifierSettings { - enable: Some(true), - ..Default::default() - }, + config.set_workspace_settings( + Default::default(), + Some( + vec![ + ( + Url::parse("file:///root1/").unwrap(), + WorkspaceSettings { + enable_paths: Some(vec![ + "sub_dir".to_string(), + "sub_dir/other".to_string(), + "test.ts".to_string(), + ]), + ..Default::default() + }, + ), + ( + Url::parse("file:///root2/").unwrap(), + WorkspaceSettings { + enable_paths: Some(vec!["other.ts".to_string()]), + ..Default::default() + }, + ), + ( + Url::parse("file:///root3/").unwrap(), + WorkspaceSettings { + enable: Some(true), + ..Default::default() + }, + ), + ] + .into_iter() + .collect(), + ), ); assert_eq!( @@ -1583,7 +1581,7 @@ mod tests { fn config_enable_via_config_file_detection() { let root_uri = resolve_url("file:///root/").unwrap(); let mut config = Config::new_with_root(root_uri.clone()); - config.settings.workspace.enable = None; + config.settings.unscoped.enable = None; assert!(!config.specifier_enabled(&root_uri)); config.set_config_file( @@ -1597,7 +1595,7 @@ mod tests { fn config_specifier_enabled_matches_by_path_component() { let root_uri = resolve_url("file:///root/").unwrap(); let mut config = Config::new_with_root(root_uri.clone()); - config.settings.workspace.enable_paths = Some(vec!["mo".to_string()]); + config.settings.unscoped.enable_paths = Some(vec!["mo".to_string()]); assert!(!config.specifier_enabled(&root_uri.join("mod.ts").unwrap())); } @@ -1605,11 +1603,11 @@ mod tests { fn config_specifier_enabled_for_test() { let root_uri = resolve_url("file:///root/").unwrap(); let mut config = Config::new_with_root(root_uri.clone()); - config.settings.workspace.enable = Some(true); + config.settings.unscoped.enable = Some(true); - config.settings.workspace.enable_paths = + config.settings.unscoped.enable_paths = Some(vec!["mod1.ts".to_string(), "mod2.ts".to_string()]); - config.settings.workspace.disable_paths = vec!["mod2.ts".to_string()]; + config.settings.unscoped.disable_paths = vec!["mod2.ts".to_string()]; assert!( config.specifier_enabled_for_test(&root_uri.join("mod1.ts").unwrap()) ); @@ -1619,7 +1617,7 @@ mod tests { assert!( !config.specifier_enabled_for_test(&root_uri.join("mod3.ts").unwrap()) ); - config.settings.workspace.enable_paths = None; + config.settings.unscoped.enable_paths = None; config.set_config_file( ConfigFile::new( @@ -1688,7 +1686,7 @@ mod tests { fn config_snapshot_specifier_enabled_for_test() { let root_uri = resolve_url("file:///root/").unwrap(); let mut config = Config::new_with_root(root_uri.clone()); - config.settings.workspace.enable = Some(true); + config.settings.unscoped.enable = Some(true); config.set_config_file( ConfigFile::new( &json!({ diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs index 37427d878d..8f96968459 100644 --- a/cli/lsp/diagnostics.rs +++ b/cli/lsp/diagnostics.rs @@ -788,37 +788,37 @@ fn generate_lint_diagnostics( let documents = snapshot .documents .documents(DocumentsFilter::OpenDiagnosable); - let workspace_settings = config.settings.workspace.clone(); let lint_rules = get_configured_rules(lint_options.rules.clone()); let mut diagnostics_vec = Vec::new(); - if workspace_settings.lint { - for document in documents { - // exit early if cancelled - if token.is_cancelled() { - break; - } - - // ignore any npm package files - if let Some(npm) = &snapshot.npm { - if npm.node_resolver.in_npm_package(document.specifier()) { - continue; - } - } - - let version = document.maybe_lsp_version(); - diagnostics_vec.push(DiagnosticRecord { - specifier: document.specifier().clone(), - versioned: VersionedDiagnostics { - version, - diagnostics: generate_document_lint_diagnostics( - config, - lint_options, - lint_rules.clone(), - &document, - ), - }, - }); + for document in documents { + let settings = + config.workspace_settings_for_specifier(document.specifier()); + if !settings.lint { + continue; } + // exit early if cancelled + if token.is_cancelled() { + break; + } + // ignore any npm package files + if let Some(npm) = &snapshot.npm { + if npm.node_resolver.in_npm_package(document.specifier()) { + continue; + } + } + let version = document.maybe_lsp_version(); + diagnostics_vec.push(DiagnosticRecord { + specifier: document.specifier().clone(), + versioned: VersionedDiagnostics { + version, + diagnostics: generate_document_lint_diagnostics( + config, + lint_options, + lint_rules.clone(), + &document, + ), + }, + }); } diagnostics_vec } @@ -1442,7 +1442,6 @@ mod tests { use crate::cache::RealDenoCacheEnv; use crate::lsp::config::ConfigSnapshot; use crate::lsp::config::Settings; - use crate::lsp::config::SpecifierSettings; use crate::lsp::config::WorkspaceSettings; use crate::lsp::documents::Documents; use crate::lsp::documents::LanguageId; @@ -1497,7 +1496,7 @@ mod tests { let root_uri = resolve_url("file:///").unwrap(); ConfigSnapshot { settings: Settings { - workspace: WorkspaceSettings { + unscoped: WorkspaceSettings { enable: Some(true), lint: true, ..Default::default() @@ -1529,7 +1528,6 @@ mod tests { #[tokio::test] async fn test_enabled_then_disabled_specifier() { let temp_dir = TempDir::new(); - let specifier = ModuleSpecifier::parse("file:///a.ts").unwrap(); let (snapshot, cache_location) = setup( &temp_dir, &[( @@ -1578,15 +1576,10 @@ let c: number = "a"; // now test disabled specifier { let mut disabled_config = mock_config(); - disabled_config.settings.specifiers.insert( - specifier.clone(), - SpecifierSettings { - enable: Some(false), - disable_paths: vec![], - enable_paths: None, - code_lens: Default::default(), - }, - ); + disabled_config.settings.unscoped = WorkspaceSettings { + enable: Some(false), + ..Default::default() + }; let diagnostics = generate_lint_diagnostics( &snapshot, diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index e6e49c654a..8afcc7ffc0 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -23,6 +23,7 @@ use import_map::ImportMap; use indexmap::IndexSet; use log::error; use serde_json::from_value; +use std::collections::BTreeMap; use std::collections::HashMap; use std::collections::HashSet; use std::env; @@ -80,7 +81,6 @@ use super::tsc::AssetsSnapshot; use super::tsc::GetCompletionDetailsArgs; use super::tsc::TsServer; use super::urls; -use super::urls::LspClientUrl; use crate::args::get_root_cert_store; use crate::args::package_json; use crate::args::resolve_import_map_from_specifier; @@ -403,68 +403,42 @@ impl LanguageServer { } } - pub async fn refresh_specifiers_from_client(&self) -> bool { - let (client, specifiers) = { + pub async fn refresh_configuration(&self) { + let (client, folders, capable) = { let ls = self.0.read().await; - let specifiers = if ls.config.client_capabilities.workspace_configuration - { - let root_capacity = std::cmp::max(ls.config.workspace_folders.len(), 1); - let config_specifiers = ls.config.get_specifiers(); - let mut specifiers = - HashMap::with_capacity(root_capacity + config_specifiers.len()); - for (specifier, folder) in &ls.config.workspace_folders { - specifiers - .insert(specifier.clone(), LspClientUrl::new(folder.uri.clone())); - } - specifiers.extend( - ls.config - .get_specifiers() - .iter() - .map(|s| (s.clone(), ls.url_map.normalize_specifier(s).unwrap())), - ); - - Some(specifiers.into_iter().collect::>()) - } else { - None - }; - - (ls.client.clone(), specifiers) + ( + ls.client.clone(), + ls.config.workspace_folders.clone(), + ls.config.client_capabilities.workspace_configuration, + ) }; - - let mut touched = false; - if let Some(specifiers) = specifiers { - let configs_result = client + if capable { + let mut scopes = Vec::with_capacity(folders.len() + 1); + scopes.push(None); + for (_, folder) in &folders { + scopes.push(Some(folder.uri.clone())); + } + let configs = client .when_outside_lsp_lock() - .specifier_configurations( - specifiers - .iter() - .map(|(_, client_uri)| client_uri.clone()) - .collect(), - ) + .workspace_configuration(scopes) .await; - - let mut ls = self.0.write().await; - if let Ok(configs) = configs_result { - for (value, internal_uri) in - configs.into_iter().zip(specifiers.into_iter().map(|s| s.0)) - { - match value { - Ok(specifier_settings) => { - if ls - .config - .set_specifier_settings(internal_uri, specifier_settings) - { - touched = true; - } - } - Err(err) => { - error!("{}", err); - } - } + if let Ok(configs) = configs { + if configs.len() != folders.len() + 1 { + lsp_warn!("Incorrect number of configurations received."); + return; } + let mut configs = configs.into_iter(); + let unscoped = configs.next().unwrap(); + let mut by_workspace_folder = BTreeMap::new(); + for (folder_uri, _) in &folders { + by_workspace_folder + .insert(folder_uri.clone(), configs.next().unwrap()); + } + let mut ls = self.0.write().await; + ls.config + .set_workspace_settings(unscoped, Some(by_workspace_folder)); } } - touched } } @@ -1182,6 +1156,7 @@ impl Inner { if let Some(options) = params.initialization_options { self.config.set_workspace_settings( WorkspaceSettings::from_initialization_options(options), + None, ); } if let Some(folders) = params.workspace_folders { @@ -1391,27 +1366,22 @@ impl Inner { async fn did_change_configuration( &mut self, - client_workspace_config: Option, params: DidChangeConfigurationParams, ) { - let maybe_config = - if self.config.client_capabilities.workspace_configuration { - client_workspace_config - } else { - params.settings.as_object().map(|settings| { - let deno = - serde_json::to_value(settings.get(SETTINGS_SECTION)).unwrap(); - let javascript = - serde_json::to_value(settings.get("javascript")).unwrap(); - let typescript = - serde_json::to_value(settings.get("typescript")).unwrap(); - WorkspaceSettings::from_raw_settings(deno, javascript, typescript) - }) - }; - - if let Some(settings) = maybe_config { - self.config.set_workspace_settings(settings); - } + if !self.config.client_capabilities.workspace_configuration { + let config = params.settings.as_object().map(|settings| { + let deno = + serde_json::to_value(settings.get(SETTINGS_SECTION)).unwrap(); + let javascript = + serde_json::to_value(settings.get("javascript")).unwrap(); + let typescript = + serde_json::to_value(settings.get("typescript")).unwrap(); + WorkspaceSettings::from_raw_settings(deno, javascript, typescript) + }); + if let Some(settings) = config { + self.config.set_workspace_settings(settings, None); + } + }; self.update_debug_flag(); if let Err(err) = self.update_cache().await { @@ -2148,8 +2118,7 @@ impl Inner { .normalize_url(¶ms.text_document.uri, LspUrlKind::File); if !self.is_diagnosable(&specifier) || !self.config.specifier_enabled(&specifier) - || !(self.config.workspace_settings().enabled_code_lens() - || self.config.specifier_code_lens_test(&specifier)) + || !self.config.enabled_code_lens_for_specifier(&specifier) { return Ok(None); } @@ -2385,10 +2354,8 @@ impl Inner { ¶ms.text_document_position.text_document.uri, LspUrlKind::File, ); - let language_settings = self - .config - .workspace_settings() - .language_settings_for_specifier(&specifier); + let language_settings = + self.config.language_settings_for_specifier(&specifier); if !self.is_diagnosable(&specifier) || !self.config.specifier_enabled(&specifier) || !language_settings.map(|s| s.suggest.enabled).unwrap_or(true) @@ -2457,7 +2424,6 @@ impl Inner { line_index, &self .config - .workspace_settings() .language_settings_for_specifier(&specifier) .cloned() .unwrap_or_default() @@ -2992,7 +2958,6 @@ impl Inner { ); let options = self .config - .workspace_settings() .language_settings_for_specifier(&old_specifier) .map(|s| s.update_imports_on_file_move.clone()) .unwrap_or_default(); @@ -3188,7 +3153,7 @@ impl tower_lsp::LanguageServer for LanguageServer { } } - self.refresh_specifiers_from_client().await; + self.refresh_configuration().await; { let mut ls = self.0.write().await; @@ -3212,58 +3177,17 @@ impl tower_lsp::LanguageServer for LanguageServer { return; } - let (client, client_uri, specifier, should_get_specifier_settings) = { - let mut inner = self.0.write().await; - let client = inner.client.clone(); - let client_uri = LspClientUrl::new(params.text_document.uri.clone()); - let specifier = inner - .url_map - .normalize_url(client_uri.as_url(), LspUrlKind::File); - let document = inner.did_open(&specifier, params).await; - let should_get_specifier_settings = - !inner.config.has_specifier_settings(&specifier) - && inner.config.client_capabilities.workspace_configuration; - if document.is_diagnosable() { - inner.refresh_npm_specifiers().await; - let specifiers = inner.documents.dependents(&specifier); - inner.diagnostics_server.invalidate(&specifiers); - // don't send diagnostics yet if we don't have the specifier settings - if !should_get_specifier_settings { - inner.send_diagnostics_update(); - inner.send_testing_update(); - } - } - (client, client_uri, specifier, should_get_specifier_settings) - }; - - // retrieve the specifier settings outside the lock if - // they haven't been asked for yet - if should_get_specifier_settings { - let response = client - .when_outside_lsp_lock() - .specifier_configuration(&client_uri) - .await; - let mut ls = self.0.write().await; - match response { - Ok(specifier_settings) => { - ls.config - .set_specifier_settings(specifier.clone(), specifier_settings); - } - Err(err) => { - error!("{}", err); - } - } - - if ls - .documents - .get(&specifier) - .map(|d| d.is_diagnosable()) - .unwrap_or(false) - { - ls.refresh_documents_config().await; - ls.send_diagnostics_update(); - ls.send_testing_update(); - } + let mut inner = self.0.write().await; + let specifier = inner + .url_map + .normalize_url(¶ms.text_document.uri, LspUrlKind::File); + let document = inner.did_open(&specifier, params).await; + if document.is_diagnosable() { + inner.refresh_npm_specifiers().await; + let specifiers = inner.documents.dependents(&specifier); + inner.diagnostics_server.invalidate(&specifiers); + inner.send_diagnostics_update(); + inner.send_testing_update(); } } @@ -3277,7 +3201,10 @@ impl tower_lsp::LanguageServer for LanguageServer { let mut inner = self.0.write().await; let specifier = inner.url_map.normalize_url(uri, LspUrlKind::File); inner.documents.save(&specifier); - if !inner.config.workspace_settings().cache_on_save + if !inner + .config + .workspace_settings_for_specifier(&specifier) + .cache_on_save || !inner.config.specifier_enabled(&specifier) || !inner.diagnostics_state.has_no_cache_diagnostics(&specifier) { @@ -3310,47 +3237,17 @@ impl tower_lsp::LanguageServer for LanguageServer { &self, params: DidChangeConfigurationParams, ) { - let (mark, has_workspace_capability, client) = { + let mark = { let inner = self.0.read().await; - ( - inner - .performance - .mark("did_change_configuration", Some(¶ms)), - inner.config.client_capabilities.workspace_configuration, - inner.client.clone(), - ) + inner + .performance + .mark("did_change_configuration", Some(¶ms)) }; - self.refresh_specifiers_from_client().await; + self.refresh_configuration().await; - // Get the configuration from the client outside of the lock - // in order to prevent potential deadlocking scenarios where - // the server holds a lock and calls into the client, which - // calls into the server which deadlocks acquiring the lock. - // There is a gap here between when the configuration is - // received and acquiring the lock, but most likely there - // won't be any racing here. - let client_workspace_config = if has_workspace_capability { - let config_response = client - .when_outside_lsp_lock() - .workspace_configuration() - .await; - match config_response { - Ok(settings) => Some(settings), - Err(err) => { - error!("{}", err); - None - } - } - } else { - None - }; - - // now update the inner state let mut inner = self.0.write().await; - inner - .did_change_configuration(client_workspace_config, params) - .await; + inner.did_change_configuration(params).await; inner.performance.measure(mark); } @@ -3374,7 +3271,8 @@ impl tower_lsp::LanguageServer for LanguageServer { (ls.performance.clone(), mark) }; - if self.refresh_specifiers_from_client().await { + self.refresh_configuration().await; + { let mut ls = self.0.write().await; ls.refresh_documents_config().await; ls.diagnostics_server.invalidate_all(); @@ -3681,10 +3579,7 @@ impl Inner { .normalize_url(¶ms.text_document.uri, LspUrlKind::File); if !self.is_diagnosable(&specifier) || !self.config.specifier_enabled(&specifier) - || !self - .config - .workspace_settings() - .enabled_inlay_hints(&specifier) + || !self.config.enabled_inlay_hints_for_specifier(&specifier) { return Ok(None); } diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 7f650348b1..94770603c8 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -4215,9 +4215,8 @@ impl UserPreferences { use_label_details_in_completion_entries: Some(true), ..Default::default() }; - let Some(language_settings) = config - .workspace_settings() - .language_settings_for_specifier(specifier) + let Some(language_settings) = + config.language_settings_for_specifier(specifier) else { return base_preferences; }; @@ -5312,7 +5311,7 @@ mod tests { .variable_types .suppress_when_type_matches_name = true; let mut config = config::Config::new(); - config.set_workspace_settings(settings); + config.set_workspace_settings(settings, None); let user_preferences = UserPreferences::from_config_for_specifier( &config, &Default::default(), diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs index 85e55ee81d..42cd11b3da 100644 --- a/cli/tests/integration/lsp_tests.rs +++ b/cli/tests/integration/lsp_tests.rs @@ -1024,23 +1024,23 @@ fn lsp_import_attributes() { client.initialize(|builder| { builder.set_import_map("data:application/json;utf8,{\"imports\": { \"example\": \"https://deno.land/x/example/mod.ts\" }}"); }); - - client.did_open_with_config( - json!({ - "textDocument": { - "uri": "file:///a/test.json", - "languageId": "json", - "version": 1, - "text": "{\"a\":1}" - } - }), - &json!({ "deno": { + client.change_configuration(json!({ + "deno": { "enable": true, "codeLens": { - "test": true - } - } }), - ); + "test": true, + }, + }, + })); + + client.did_open(json!({ + "textDocument": { + "uri": "file:///a/test.json", + "languageId": "json", + "version": 1, + "text": "{\"a\":1}", + }, + })); let diagnostics = client.did_open(json!({ "textDocument": { @@ -1380,17 +1380,15 @@ fn lsp_hover_disabled() { client.initialize(|builder| { builder.set_deno_enable(false); }); - client.did_open_with_config( - json!({ - "textDocument": { - "uri": "file:///a/file.ts", - "languageId": "typescript", - "version": 1, - "text": "console.log(Date.now());\n" - } - }), - &json!({ "deno": { "enable": false } }), - ); + client.change_configuration(json!({ "deno": { "enable": false } })); + client.did_open(json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "console.log(Date.now());\n", + }, + })); let res = client.write_request( "textDocument/hover", @@ -3794,24 +3792,22 @@ fn lsp_code_lens_test_disabled() { "test": false }))); }); - client - .did_open_with_config( - json!({ - "textDocument": { - "uri": "file:///a/file.ts", - "languageId": "typescript", - "version": 1, - "text": "const { test } = Deno;\nconst { test: test2 } = Deno;\nconst test3 = Deno.test;\n\nDeno.test(\"test a\", () => {});\nDeno.test({\n name: \"test b\",\n fn() {},\n});\ntest({\n name: \"test c\",\n fn() {},\n});\ntest(\"test d\", () => {});\ntest2({\n name: \"test e\",\n fn() {},\n});\ntest2(\"test f\", () => {});\ntest3({\n name: \"test g\",\n fn() {},\n});\ntest3(\"test h\", () => {});\n" - } - }), - // disable test code lens - &json!({ "deno": { - "enable": true, - "codeLens": { - "test": false - } - } }), - ); + client.change_configuration(json!({ + "deno": { + "enable": true, + "codeLens": { + "test": false, + }, + }, + })); + client.did_open(json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "const { test } = Deno;\nconst { test: test2 } = Deno;\nconst test3 = Deno.test;\n\nDeno.test(\"test a\", () => {});\nDeno.test({\n name: \"test b\",\n fn() {},\n});\ntest({\n name: \"test c\",\n fn() {},\n});\ntest(\"test d\", () => {});\ntest2({\n name: \"test e\",\n fn() {},\n});\ntest2(\"test f\", () => {});\ntest3({\n name: \"test g\",\n fn() {},\n});\ntest3(\"test h\", () => {});\n" + }, + })); let res = client.write_request( "textDocument/codeLens", json!({ @@ -3820,7 +3816,7 @@ fn lsp_code_lens_test_disabled() { } }), ); - assert_eq!(res, json!([])); + assert_eq!(res, json!(null)); client.shutdown(); } @@ -4898,22 +4894,12 @@ fn lsp_cache_on_save() { ); let mut client = context.new_lsp_command().build(); client.initialize_default(); - client.write_notification( - "workspace/didChangeConfiguration", - json!({ - "settings": {} - }), - ); - let settings = json!({ + client.change_configuration(json!({ "deno": { "enable": true, "cacheOnSave": true, }, - }); - // one for the workspace - client.handle_configuration_request(&settings); - // one for the specifier - client.handle_configuration_request(&settings); + })); let diagnostics = client.did_open(json!({ "textDocument": { @@ -5592,23 +5578,16 @@ fn lsp_quote_style_from_workspace_settings() { ); let mut client = context.new_lsp_command().build(); client.initialize_default(); - client.write_notification( - "workspace/didChangeConfiguration", - json!({ - "settings": {} - }), - ); - let settings = json!({ + client.change_configuration(json!({ + "deno": { + "enable": true, + }, "typescript": { "preferences": { "quoteStyle": "single", }, }, - }); - // one for the workspace - client.handle_configuration_request(&settings); - // one for the specifier - client.handle_configuration_request(&settings); + })); let code_action_params = json!({ "textDocument": { @@ -5792,7 +5771,7 @@ fn lsp_code_actions_deadlock() { let large_file_text = fs::read_to_string(testdata_path().join("lsp").join("large_file.txt")) .unwrap(); - client.did_open_raw(json!({ + client.did_open(json!({ "textDocument": { "uri": "file:///a/file.ts", "languageId": "javascript", @@ -5800,7 +5779,6 @@ fn lsp_code_actions_deadlock() { "text": large_file_text, } })); - client.handle_configuration_request(&json!({ "deno": { "enable": true } })); client.write_request( "textDocument/semanticTokens/full", json!({ @@ -5809,7 +5787,6 @@ fn lsp_code_actions_deadlock() { } }), ); - client.read_diagnostics(); client.write_notification( "textDocument/didChange", json!({ @@ -8901,17 +8878,11 @@ fn lsp_configuration_did_change() { "text": "import * as a from \"http://localhost:4545/x/a@\"" } })); - client.write_notification( - "workspace/didChangeConfiguration", - json!({ - "settings": {} - }), - ); - let settings = json!({ "deno": { + client.change_configuration(json!({ "deno": { "enable": true, "codeLens": { "implementations": true, - "references": true + "references": true, }, "importMap": null, "lint": true, @@ -8922,16 +8893,12 @@ fn lsp_configuration_did_change() { "paths": true, "imports": { "hosts": { - "http://localhost:4545/": true - } - } + "http://localhost:4545/": true, + }, + }, }, - "unstable": false - } }); - // one for the workspace - client.handle_configuration_request(&settings); - // one for the specifier - client.handle_configuration_request(&settings); + "unstable": false, + } })); let list = client.get_completion_list( "file:///a/file.ts", @@ -8997,13 +8964,7 @@ fn lsp_completions_complete_function_calls() { "text": "[]." } })); - client.write_notification( - "workspace/didChangeConfiguration", - json!({ - "settings": {} - }), - ); - let settings = json!({ + client.change_configuration(json!({ "deno": { "enable": true, }, @@ -9012,11 +8973,7 @@ fn lsp_completions_complete_function_calls() { "completeFunctionCalls": true, }, }, - }); - // one for the workspace - client.handle_configuration_request(&settings); - // one for the specifier - client.handle_configuration_request(&settings); + })); let list = client.get_completion_list( "file:///a/file.ts", @@ -10099,22 +10056,12 @@ fn lsp_node_modules_dir() { "{ \"nodeModulesDir\": true, \"lock\": false }\n", ); let refresh_config = |client: &mut LspClient| { - client.write_notification( - "workspace/didChangeConfiguration", - json!({ - "settings": { - "enable": true, - "config": "./deno.json", - } - }), - ); - - let settings = json!({ "deno": { + client.change_configuration(json!({ "deno": { "enable": true, "config": "./deno.json", "codeLens": { "implementations": true, - "references": true + "references": true, }, "importMap": null, "lint": false, @@ -10123,14 +10070,10 @@ fn lsp_node_modules_dir() { "completeFunctionCalls": false, "names": true, "paths": true, - "imports": {} + "imports": {}, }, - "unstable": false - } }); - // one for the workspace - client.handle_configuration_request(&settings); - // one for the specifier - client.handle_configuration_request(&settings); + "unstable": false, + } })); }; refresh_config(&mut client); @@ -10228,22 +10171,12 @@ fn lsp_vendor_dir() { "{ \"vendor\": true, \"lock\": false }\n", ); let refresh_config = |client: &mut LspClient| { - client.write_notification( - "workspace/didChangeConfiguration", - json!({ - "settings": { - "enable": true, - "config": "./deno.json", - } - }), - ); - - let settings = json!({ "deno": { + client.change_configuration(json!({ "deno": { "enable": true, "config": "./deno.json", "codeLens": { "implementations": true, - "references": true + "references": true, }, "importMap": null, "lint": false, @@ -10252,14 +10185,10 @@ fn lsp_vendor_dir() { "completeFunctionCalls": false, "names": true, "paths": true, - "imports": {} + "imports": {}, }, - "unstable": false - } }); - // one for the workspace - client.handle_configuration_request(&settings); - // one for the specifier - client.handle_configuration_request(&settings); + "unstable": false, + } })); }; refresh_config(&mut client); diff --git a/test_util/src/lsp.rs b/test_util/src/lsp.rs index 4e87503f2c..352f5d3656 100644 --- a/test_util/src/lsp.rs +++ b/test_util/src/lsp.rs @@ -210,7 +210,22 @@ pub struct InitializeParamsBuilder { impl InitializeParamsBuilder { #[allow(clippy::new_without_default)] - pub fn new() -> Self { + pub fn new(config: Value) -> Self { + let mut config_as_options = json!({}); + if let Some(object) = config.as_object() { + if let Some(deno) = object.get("deno") { + if let Some(deno) = deno.as_object() { + config_as_options = json!(deno.clone()); + } + } + let config_as_options = config_as_options.as_object_mut().unwrap(); + if let Some(typescript) = object.get("typescript") { + config_as_options.insert("typescript".to_string(), typescript.clone()); + } + if let Some(javascript) = object.get("javascript") { + config_as_options.insert("javascript".to_string(), javascript.clone()); + } + } Self { params: InitializeParams { process_id: None, @@ -219,38 +234,7 @@ impl InitializeParamsBuilder { version: Some("1.0.0".to_string()), }), root_uri: None, - initialization_options: Some(json!({ - "enableBuiltinCommands": true, - "enable": true, - "cache": null, - "certificateStores": null, - "codeLens": { - "implementations": true, - "references": true, - "test": true - }, - "config": null, - "importMap": null, - "lint": true, - "suggest": { - "autoImports": true, - "completeFunctionCalls": false, - "names": true, - "paths": true, - "imports": { - "hosts": {} - } - }, - "testing": { - "args": [ - "--allow-all" - ], - "enable": true - }, - "tlsCertificate": null, - "unsafelyIgnoreCertificateErrors": null, - "unstable": false - })), + initialization_options: Some(config_as_options), capabilities: ClientCapabilities { text_document: Some(TextDocumentClientCapabilities { code_action: Some(CodeActionClientCapabilities { @@ -686,21 +670,70 @@ impl LspClient { ) { self.initialize_with_config( do_build, - json!({"deno":{ - "enable": true - }}), + json!({ "deno": { + "enableBuiltinCommands": true, + "enable": true, + "cache": null, + "certificateStores": null, + "codeLens": { + "implementations": true, + "references": true, + "test": true, + }, + "config": null, + "importMap": null, + "lint": true, + "suggest": { + "autoImports": true, + "completeFunctionCalls": false, + "names": true, + "paths": true, + "imports": { + "hosts": {}, + }, + }, + "testing": { + "args": [ + "--allow-all" + ], + "enable": true, + }, + "tlsCertificate": null, + "unsafelyIgnoreCertificateErrors": null, + "unstable": false, + } }), ) } pub fn initialize_with_config( &mut self, do_build: impl Fn(&mut InitializeParamsBuilder), - config: Value, + mut config: Value, ) { - let mut builder = InitializeParamsBuilder::new(); + let mut builder = InitializeParamsBuilder::new(config.clone()); builder.set_root_uri(self.context.temp_dir().uri()); do_build(&mut builder); let params: InitializeParams = builder.build(); + // `config` must be updated to account for the builder changes. + // TODO(nayeemrmn): Remove config-related methods from builder. + if let Some(options) = ¶ms.initialization_options { + if let Some(options) = options.as_object() { + if let Some(config) = config.as_object_mut() { + let mut deno = options.clone(); + let typescript = options.get("typescript"); + let javascript = options.get("javascript"); + deno.remove("typescript"); + deno.remove("javascript"); + config.insert("deno".to_string(), json!(deno)); + if let Some(typescript) = typescript { + config.insert("typescript".to_string(), typescript.clone()); + } + if let Some(javascript) = javascript { + config.insert("javascript".to_string(), javascript.clone()); + } + } + } + } self.supports_workspace_configuration = match ¶ms.capabilities.workspace { Some(workspace) => workspace.configuration == Some(true), @@ -710,23 +743,12 @@ impl LspClient { self.write_notification("initialized", json!({})); self.config = config; if self.supports_workspace_configuration { - self.handle_configuration_request(&self.config.clone()); + self.handle_configuration_request(); } } pub fn did_open(&mut self, params: Value) -> CollectedDiagnostics { - self.did_open_with_config(params, &self.config.clone()) - } - - pub fn did_open_with_config( - &mut self, - params: Value, - config: &Value, - ) -> CollectedDiagnostics { self.did_open_raw(params); - if self.supports_workspace_configuration { - self.handle_configuration_request(config); - } self.read_diagnostics() } @@ -734,17 +756,33 @@ impl LspClient { self.write_notification("textDocument/didOpen", params); } - pub fn handle_configuration_request(&mut self, settings: &Value) { + pub fn change_configuration(&mut self, config: Value) { + self.config = config; + if self.supports_workspace_configuration { + self.write_notification( + "workspace/didChangeConfiguration", + json!({ "settings": {} }), + ); + self.handle_configuration_request(); + } else { + self.write_notification( + "workspace/didChangeConfiguration", + json!({ "settings": &self.config }), + ); + } + } + + pub fn handle_configuration_request(&mut self) { let (id, method, args) = self.read_request::(); assert_eq!(method, "workspace/configuration"); let params = args.as_ref().unwrap().as_object().unwrap(); let items = params.get("items").unwrap().as_array().unwrap(); - let settings_object = settings.as_object().unwrap(); + let config_object = self.config.as_object().unwrap(); let mut result = vec![]; for item in items { let item = item.as_object().unwrap(); let section = item.get("section").unwrap().as_str().unwrap(); - result.push(settings_object.get(section).cloned().unwrap_or_default()); + result.push(config_object.get(section).cloned().unwrap_or_default()); } self.write_response(id, result); }