diff --git a/crates/paths/src/lib.rs b/crates/paths/src/lib.rs index f0a6bd4f7e30..d00d903a2732 100644 --- a/crates/paths/src/lib.rs +++ b/crates/paths/src/lib.rs @@ -150,7 +150,7 @@ impl AbsPathBuf { /// * if `self` has a verbatim prefix (e.g. `\\?\C:\windows`) /// and `path` is not empty, the new path is normalized: all references /// to `.` and `..` are removed. - pub fn push(&mut self, suffix: &str) { + pub fn push>(&mut self, suffix: P) { self.0.push(suffix) } } diff --git a/crates/rust-analyzer/src/bin/main.rs b/crates/rust-analyzer/src/bin/main.rs index c620fc3d0866..725c86611230 100644 --- a/crates/rust-analyzer/src/bin/main.rs +++ b/crates/rust-analyzer/src/bin/main.rs @@ -229,7 +229,7 @@ fn run_server() -> anyhow::Result<()> { let mut change = ConfigChange::default(); change.change_client_config(json); let mut error_sink = ConfigError::default(); - config = config.apply_change(change, &mut error_sink); + (config, _) = config.apply_change(change, &mut error_sink); if !error_sink.is_empty() { use lsp_types::{ notification::{Notification, ShowMessage}, diff --git a/crates/rust-analyzer/src/cli/scip.rs b/crates/rust-analyzer/src/cli/scip.rs index b2d70562890f..d73e4028e518 100644 --- a/crates/rust-analyzer/src/cli/scip.rs +++ b/crates/rust-analyzer/src/cli/scip.rs @@ -46,7 +46,7 @@ impl flags::Scip { let mut change = ConfigChange::default(); change.change_client_config(json); let mut error_sink = ConfigError::default(); - config = config.apply_change(change, &mut error_sink); + (config, _) = config.apply_change(change, &mut error_sink); // FIXME @alibektas : What happens to errors without logging? error!(?error_sink, "Config Error(s)"); } diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index 8e31a11ca565..a0204190103b 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -29,12 +29,11 @@ use rustc_hash::{FxHashMap, FxHashSet}; use semver::Version; use serde::{ de::{DeserializeOwned, Error}, - ser::SerializeStruct, Deserialize, Serialize, }; use stdx::format_to_acc; use triomphe::Arc; -use vfs::{AbsPath, AbsPathBuf, FileId, VfsPath}; +use vfs::{AbsPath, AbsPathBuf, VfsPath}; use crate::{ caps::completion_item_edit_resolve, @@ -663,7 +662,7 @@ pub struct Config { default_config: DefaultConfigData, /// Config node that obtains its initial value during the server initialization and /// by receiving a `lsp_types::notification::DidChangeConfiguration`. - client_config: ClientConfig, + client_config: FullConfigInput, /// Path to the root configuration file. This can be seen as a generic way to define what would be `$XDG_CONFIG_HOME/rust-analyzer/rust-analyzer.toml` in Linux. /// If not specified by init of a `Config` object this value defaults to : @@ -677,139 +676,48 @@ pub struct Config { /// FIXME @alibektas : Change this to sth better. /// Config node whose values apply to **every** Rust project. - user_config: Option, + user_config: Option, /// A special file for this session whose path is set to `self.root_path.join("rust-analyzer.toml")` root_ratoml_path: VfsPath, /// This file can be used to make global changes while having only a workspace-wide scope. - root_ratoml: Option, + root_ratoml: Option, /// For every `SourceRoot` there can be at most one RATOML file. - ratoml_files: FxHashMap, + ratoml_files: FxHashMap, /// Clone of the value that is stored inside a `GlobalState`. source_root_parent_map: Arc>, - /// Changes made to client and global configurations will partially not be reflected even after `.apply_change()` was called. - /// This field signals that the `GlobalState` should call its `update_configuration()` method. - should_update: bool, -} - -#[derive(Clone, Debug)] -struct RatomlNode { - node: GlobalLocalConfigInput, - file_id: FileId, -} - -#[derive(Debug, Clone, Default)] -struct ClientConfig { - node: FullConfigInput, detached_files: Vec, } -impl Serialize for RatomlNode { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let mut s = serializer.serialize_struct("RatomlNode", 2)?; - s.serialize_field("file_id", &self.file_id.index())?; - s.serialize_field("config", &self.node)?; - s.end() - } -} - -#[derive(Debug, Hash, Eq, PartialEq)] -pub(crate) enum ConfigNodeKey { - Ratoml(SourceRootId), - Client, - User, -} - -impl Serialize for ConfigNodeKey { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self { - ConfigNodeKey::Ratoml(source_root_id) => serializer.serialize_u32(source_root_id.0), - ConfigNodeKey::Client => serializer.serialize_str("client"), - ConfigNodeKey::User => serializer.serialize_str("user"), - } - } -} - -#[derive(Debug, Serialize)] -enum ConfigNodeValue<'a> { - /// `rust-analyzer::config` module works by setting - /// a mapping between `SourceRootId` and `ConfigInput`. - /// Storing a `FileId` is mostly for debugging purposes. - Ratoml(&'a RatomlNode), - Client(&'a FullConfigInput), -} - impl Config { - /// FIXME @alibektas : Before integration tests, I thought I would - /// get the debug output of the config tree and do assertions based on it. - /// The reason why I didn't delete this is that we may want to have a lsp_ext - /// like "DebugConfigTree" so that it is easier for users to get a snapshot of - /// the config state for us to debug. - #[allow(dead_code)] - /// Walk towards the root starting from a specified `ConfigNode` - fn traverse( - &self, - start: ConfigNodeKey, - ) -> impl Iterator)> { - let mut v = vec![]; - - if let ConfigNodeKey::Ratoml(start) = start { - let mut par: Option = Some(start); - while let Some(source_root_id) = par { - par = self.source_root_parent_map.get(&start).copied(); - if let Some(config) = self.ratoml_files.get(&source_root_id) { - v.push(( - ConfigNodeKey::Ratoml(source_root_id), - ConfigNodeValue::Ratoml(config), - )); - } - } - } - - v.push((ConfigNodeKey::Client, ConfigNodeValue::Client(&self.client_config.node))); - - if let Some(user_config) = self.user_config.as_ref() { - v.push((ConfigNodeKey::User, ConfigNodeValue::Ratoml(user_config))); - } - - v.into_iter() - } - pub fn user_config_path(&self) -> &VfsPath { &self.user_config_path } - pub fn should_update(&self) -> bool { - self.should_update - } - // FIXME @alibektas : Server's health uses error sink but in other places it is not used atm. - pub fn apply_change(&self, change: ConfigChange, error_sink: &mut ConfigError) -> Config { + /// Changes made to client and global configurations will partially not be reflected even after `.apply_change()` was called. + /// The return tuple's bool component signals whether the `GlobalState` should call its `update_configuration()` method. + pub fn apply_change( + &self, + change: ConfigChange, + error_sink: &mut ConfigError, + ) -> (Config, bool) { let mut config = self.clone(); let mut toml_errors = vec![]; let mut json_errors = vec![]; - config.should_update = false; - - if let Some((file_id, change)) = change.user_config_change { - config.user_config = Some(RatomlNode { - file_id, - node: GlobalLocalConfigInput::from_toml( - toml::from_str(change.to_string().as_str()).unwrap(), - &mut toml_errors, - ), - }); - config.should_update = true; + let mut should_update = false; + + if let Some(change) = change.user_config_change { + if let Ok(change) = toml::from_str(&change) { + config.user_config = + Some(GlobalLocalConfigInput::from_toml(change, &mut toml_errors)); + should_update = true; + } } if let Some(mut json) = change.client_config_change { @@ -828,38 +736,29 @@ impl Config { patch_old_style::patch_json_for_outdated_configs(&mut json); - config.client_config = ClientConfig { - node: FullConfigInput::from_json(json, &mut json_errors), - detached_files, - } + config.client_config = FullConfigInput::from_json(json, &mut json_errors); + config.detached_files = detached_files; } - config.should_update = true; + should_update = true; } - if let Some((file_id, change)) = change.root_ratoml_change { - config.root_ratoml = Some(RatomlNode { - file_id, - node: GlobalLocalConfigInput::from_toml( - toml::from_str(change.to_string().as_str()).unwrap(), - &mut toml_errors, - ), - }); - config.should_update = true; + if let Some(change) = change.root_ratoml_change { + if let Ok(change) = toml::from_str(&change) { + config.root_ratoml = + Some(GlobalLocalConfigInput::from_toml(change, &mut toml_errors)); + should_update = true; + } } if let Some(change) = change.ratoml_file_change { - for (source_root_id, (file_id, _, text)) in change { + for (source_root_id, (_, text)) in change { if let Some(text) = text { - config.ratoml_files.insert( - source_root_id, - RatomlNode { - file_id, - node: GlobalLocalConfigInput::from_toml( - toml::from_str(&text).unwrap(), - &mut toml_errors, - ), - }, - ); + if let Ok(change) = toml::from_str(&text) { + config.ratoml_files.insert( + source_root_id, + GlobalLocalConfigInput::from_toml(change, &mut toml_errors), + ); + } } } } @@ -903,16 +802,22 @@ impl Config { serde_json::Error::custom("expected a non-empty string"), )); } - config + (config, should_update) + } + + pub fn apply_change_whatever(&self, change: ConfigChange) -> (Config, ConfigError) { + let mut e = ConfigError(vec![]); + let (config, _) = self.apply_change(change, &mut e); + (config, e) } } #[derive(Default, Debug)] pub struct ConfigChange { - user_config_change: Option<(FileId, String)>, - root_ratoml_change: Option<(FileId, String)>, + user_config_change: Option, + root_ratoml_change: Option, client_config_change: Option, - ratoml_file_change: Option)>>, + ratoml_file_change: Option)>>, source_map_change: Option>>, } @@ -920,26 +825,25 @@ impl ConfigChange { pub fn change_ratoml( &mut self, source_root: SourceRootId, - file_id: FileId, vfs_path: VfsPath, content: Option, - ) -> Option<(FileId, VfsPath, Option)> { + ) -> Option<(VfsPath, Option)> { if let Some(changes) = self.ratoml_file_change.as_mut() { - changes.insert(source_root, (file_id, vfs_path, content)) + changes.insert(source_root, (vfs_path, content)) } else { let mut map = FxHashMap::default(); - map.insert(source_root, (file_id, vfs_path, content)); + map.insert(source_root, (vfs_path, content)); self.ratoml_file_change = Some(map); None } } - pub fn change_user_config(&mut self, content: Option<(FileId, String)>) { + pub fn change_user_config(&mut self, content: Option) { assert!(self.user_config_change.is_none()); // Otherwise it is a double write. self.user_config_change = content; } - pub fn change_root_ratoml(&mut self, content: Option<(FileId, String)>) { + pub fn change_root_ratoml(&mut self, content: Option) { assert!(self.user_config_change.is_none()); // Otherwise it is a double write. self.root_ratoml_change = content; } @@ -1159,8 +1063,6 @@ impl ConfigError { } } -impl ConfigError {} - impl fmt::Display for ConfigError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let errors = self.0.iter().format_with("\n", |inner, f| match inner { @@ -1217,7 +1119,7 @@ impl Config { snippets: Default::default(), workspace_roots, visual_studio_code_version, - client_config: ClientConfig::default(), + client_config: FullConfigInput::default(), user_config: None, ratoml_files: FxHashMap::default(), default_config: DefaultConfigData::default(), @@ -1225,7 +1127,7 @@ impl Config { user_config_path, root_ratoml: None, root_ratoml_path, - should_update: false, + detached_files: Default::default(), } } @@ -1312,7 +1214,7 @@ impl Config { pub fn detached_files(&self) -> &Vec { // FIXME @alibektas : This is the only config that is confusing. If it's a proper configuration // why is it not among the others? If it's client only which I doubt it is current state should be alright - &self.client_config.detached_files + &self.detached_files } pub fn diagnostics(&self, source_root: Option) -> DiagnosticsConfig { @@ -2580,19 +2482,19 @@ macro_rules! _impl_for_config_data { while let Some(source_root_id) = par { par = self.source_root_parent_map.get(&source_root_id).copied(); if let Some(config) = self.ratoml_files.get(&source_root_id) { - if let Some(value) = config.node.local.$field.as_ref() { + if let Some(value) = config.local.$field.as_ref() { return value; } } } } - if let Some(v) = self.client_config.node.local.$field.as_ref() { + if let Some(v) = self.client_config.local.$field.as_ref() { return &v; } if let Some(user_config) = self.user_config.as_ref() { - if let Some(v) = user_config.node.local.$field.as_ref() { + if let Some(v) = user_config.local.$field.as_ref() { return &v; } } @@ -2614,17 +2516,17 @@ macro_rules! _impl_for_config_data { $vis fn $field(&self) -> &$ty { if let Some(root_path_ratoml) = self.root_ratoml.as_ref() { - if let Some(v) = root_path_ratoml.node.global.$field.as_ref() { + if let Some(v) = root_path_ratoml.global.$field.as_ref() { return &v; } } - if let Some(v) = self.client_config.node.global.$field.as_ref() { + if let Some(v) = self.client_config.global.$field.as_ref() { return &v; } if let Some(user_config) = self.user_config.as_ref() { - if let Some(v) = user_config.node.global.$field.as_ref() { + if let Some(v) = user_config.global.$field.as_ref() { return &v; } } @@ -2666,7 +2568,7 @@ macro_rules! _config_data { }) => { /// Default config values for this grouping. #[allow(non_snake_case)] - #[derive(Debug, Clone, Serialize)] + #[derive(Debug, Clone )] struct $name { $($field: $ty,)* } impl_for_config_data!{ @@ -3392,7 +3294,7 @@ mod tests { }})); let mut error_sink = ConfigError::default(); - config = config.apply_change(change, &mut error_sink); + (config, _) = config.apply_change(change, &mut error_sink); assert_eq!(config.proc_macro_srv(), None); } @@ -3412,7 +3314,7 @@ mod tests { }})); let mut error_sink = ConfigError::default(); - config = config.apply_change(change, &mut error_sink); + (config, _) = config.apply_change(change, &mut error_sink); assert_eq!(config.proc_macro_srv(), Some(AbsPathBuf::try_from(project_root()).unwrap())); } @@ -3434,7 +3336,7 @@ mod tests { }})); let mut error_sink = ConfigError::default(); - config = config.apply_change(change, &mut error_sink); + (config, _) = config.apply_change(change, &mut error_sink); assert_eq!( config.proc_macro_srv(), @@ -3459,7 +3361,7 @@ mod tests { })); let mut error_sink = ConfigError::default(); - config = config.apply_change(change, &mut error_sink); + (config, _) = config.apply_change(change, &mut error_sink); assert_eq!(config.cargo_targetDir(), &None); assert!( matches!(config.flycheck(), FlycheckConfig::CargoCommand { options, .. } if options.target_dir.is_none()) @@ -3482,7 +3384,7 @@ mod tests { })); let mut error_sink = ConfigError::default(); - config = config.apply_change(change, &mut error_sink); + (config, _) = config.apply_change(change, &mut error_sink); assert_eq!(config.cargo_targetDir(), &Some(TargetDirectory::UseSubdirectory(true))); assert!( @@ -3506,7 +3408,7 @@ mod tests { })); let mut error_sink = ConfigError::default(); - config = config.apply_change(change, &mut error_sink); + (config, _) = config.apply_change(change, &mut error_sink); assert_eq!( config.cargo_targetDir(), diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs index 9a865b354e4b..d1668498953e 100644 --- a/crates/rust-analyzer/src/global_state.rs +++ b/crates/rust-analyzer/src/global_state.rs @@ -9,7 +9,7 @@ use crossbeam_channel::{unbounded, Receiver, Sender}; use flycheck::FlycheckHandle; use hir::ChangeWithProcMacros; use ide::{Analysis, AnalysisHost, Cancellable, FileId, SourceRootId}; -use ide_db::base_db::{CrateId, ProcMacroPaths}; +use ide_db::base_db::{CrateId, ProcMacroPaths, SourceDatabaseExt}; use load_cargo::SourceRootConfig; use lsp_types::{SemanticTokens, Url}; use nohash_hasher::IntMap; @@ -267,8 +267,8 @@ impl GlobalState { let mut ratoml_text_map: FxHashMap)> = FxHashMap::default(); - let mut user_config_file: Option<(FileId, Option)> = None; - let mut root_path_ratoml: Option<(FileId, Option)> = None; + let mut user_config_file: Option> = None; + let mut root_path_ratoml: Option> = None; let root_vfs_path = { let mut root_vfs_path = self.config.root_path().to_path_buf(); @@ -367,30 +367,23 @@ impl GlobalState { bytes.push((file.file_id, text)); } let (vfs, line_endings_map) = &mut *RwLockUpgradableReadGuard::upgrade(guard); - bytes.into_iter().for_each(|(file_id, text)| match text { - None => { - change.change_file(file_id, None); - if let Some(vfs_path) = modified_ratoml_files.get(&file_id) { - if vfs_path == self.config.user_config_path() { - user_config_file = Some((file_id, None)); - } else if vfs_path == &root_vfs_path { - root_path_ratoml = Some((file_id, None)); - } else { - ratoml_text_map.insert(file_id, (vfs_path.clone(), None)); - } + bytes.into_iter().for_each(|(file_id, text)| { + let text = match text { + None => None, + Some((text, line_endings)) => { + line_endings_map.insert(file_id, line_endings); + Some(text) } - } - Some((text, line_endings)) => { - line_endings_map.insert(file_id, line_endings); - change.change_file(file_id, Some(text.clone())); - if let Some(vfs_path) = modified_ratoml_files.get(&file_id) { - if vfs_path == self.config.user_config_path() { - user_config_file = Some((file_id, Some(text.clone()))); - } else if vfs_path == &root_vfs_path { - root_path_ratoml = Some((file_id, Some(text.clone()))); - } else { - ratoml_text_map.insert(file_id, (vfs_path.clone(), Some(text.clone()))); - } + }; + + change.change_file(file_id, text.clone()); + if let Some(vfs_path) = modified_ratoml_files.get(&file_id) { + if vfs_path == self.config.user_config_path() { + user_config_file = Some(text); + } else if vfs_path == &root_vfs_path { + root_path_ratoml = Some(text); + } else { + ratoml_text_map.insert(file_id, (vfs_path.clone(), text)); } } }); @@ -403,53 +396,54 @@ impl GlobalState { let _p = span!(Level::INFO, "GlobalState::process_changes/apply_change").entered(); self.analysis_host.apply_change(change); - - let config_change = { - let mut change = ConfigChange::default(); - let snap = self.analysis_host.analysis(); - - for (file_id, (vfs_path, text)) in ratoml_text_map { - // If change has been made to a ratoml file that - // belongs to a non-local source root, we will ignore it. - // As it doesn't make sense a users to use external config files. - if let Ok(source_root) = snap.source_root_id(file_id) { - if let Ok(true) = snap.is_local_source_root(source_root) { - if let Some((old_file, old_path, old_text)) = - change.change_ratoml(source_root, file_id, vfs_path.clone(), text) + if !(ratoml_text_map.is_empty() && user_config_file.is_none() && root_path_ratoml.is_none()) + { + let config_change = { + let mut change = ConfigChange::default(); + let db = self.analysis_host.raw_database(); + + for (file_id, (vfs_path, text)) in ratoml_text_map { + // If change has been made to a ratoml file that + // belongs to a non-local source root, we will ignore it. + // As it doesn't make sense a users to use external config files. + let sr_id = db.file_source_root(file_id); + let sr = db.source_root(sr_id); + if !sr.is_library { + if let Some((old_path, old_text)) = + change.change_ratoml(sr_id, vfs_path.clone(), text) { // SourceRoot has more than 1 RATOML files. In this case lexicographically smaller wins. if old_path < vfs_path { span!(Level::ERROR, "Two `rust-analyzer.toml` files were found inside the same crate. {vfs_path} has no effect."); // Put the old one back in. - change.change_ratoml(source_root, old_file, old_path, old_text); + change.change_ratoml(sr_id, old_path, old_text); } } + } else { + // Mapping to a SourceRoot should always end up in `Ok` + span!(Level::ERROR, "Mapping to SourceRootId failed."); } - } else { - // Mapping to a SourceRoot should always end up in `Ok` - span!(Level::ERROR, "Mapping to SourceRootId failed."); } - } - - if let Some((file_id, Some(txt))) = user_config_file { - change.change_user_config(Some((file_id, txt))); - } - if let Some((file_id, Some(txt))) = root_path_ratoml { - change.change_root_ratoml(Some((file_id, txt))); - } + if let Some(Some(txt)) = user_config_file { + change.change_user_config(Some(txt)); + } - change - }; + if let Some(Some(txt)) = root_path_ratoml { + change.change_root_ratoml(Some(txt)); + } - let mut error_sink = ConfigError::default(); - let config = self.config.apply_change(config_change, &mut error_sink); + change + }; + let mut error_sink = ConfigError::default(); + let (config, should_update) = self.config.apply_change(config_change, &mut error_sink); - if config.should_update() { - self.update_configuration(config); - } else { - // No global or client level config was changed. So we can just naively replace config. - self.config = Arc::new(config); + if should_update { + self.update_configuration(config); + } else { + // No global or client level config was changed. So we can just naively replace config. + self.config = Arc::new(config); + } } { diff --git a/crates/rust-analyzer/src/handlers/notification.rs b/crates/rust-analyzer/src/handlers/notification.rs index 6f8a5af38e89..a0c70c60c7b9 100644 --- a/crates/rust-analyzer/src/handlers/notification.rs +++ b/crates/rust-analyzer/src/handlers/notification.rs @@ -201,7 +201,8 @@ pub(crate) fn handle_did_change_configuration( let mut change = ConfigChange::default(); change.change_client_config(json.take()); let mut error_sink = ConfigError::default(); - config = config.apply_change(change, &mut error_sink); + (config, _) = config.apply_change(change, &mut error_sink); + // Client config changes neccesitates .update_config method to be called. this.update_configuration(config); } } diff --git a/crates/rust-analyzer/src/lib.rs b/crates/rust-analyzer/src/lib.rs index d9b31550c562..b3c11d0156ed 100644 --- a/crates/rust-analyzer/src/lib.rs +++ b/crates/rust-analyzer/src/lib.rs @@ -39,7 +39,7 @@ pub mod tracing { } pub mod config; -pub mod global_state; +mod global_state; pub mod lsp; use self::lsp::ext as lsp_ext; diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs index 38a534576c1e..57dc69f02571 100644 --- a/crates/rust-analyzer/src/reload.rs +++ b/crates/rust-analyzer/src/reload.rs @@ -622,7 +622,8 @@ impl GlobalState { let mut config_change = ConfigChange::default(); config_change.change_source_root_parent_map(self.local_roots_parent_map.clone()); let mut error_sink = ConfigError::default(); - self.config = Arc::new(self.config.apply_change(config_change, &mut error_sink)); + let (config, _) = self.config.apply_change(config_change, &mut error_sink); + self.config = Arc::new(config); self.recreate_crate_graph(cause); diff --git a/crates/rust-analyzer/tests/slow-tests/main.rs b/crates/rust-analyzer/tests/slow-tests/main.rs index 4836ae107812..6ccca114f90c 100644 --- a/crates/rust-analyzer/tests/slow-tests/main.rs +++ b/crates/rust-analyzer/tests/slow-tests/main.rs @@ -11,36 +11,33 @@ #![warn(rust_2018_idioms, unused_lifetimes)] #![allow(clippy::disallowed_types)] +mod ratoml; #[cfg(not(feature = "in-rust-tree"))] mod sourcegen; mod support; mod testdir; mod tidy; -use std::{collections::HashMap, path::PathBuf, sync::Once, time::Instant}; +use std::{collections::HashMap, path::PathBuf, time::Instant}; use lsp_types::{ - notification::{DidChangeTextDocument, DidOpenTextDocument, DidSaveTextDocument}, + notification::{DidOpenTextDocument}, request::{ CodeActionRequest, Completion, Formatting, GotoTypeDefinition, HoverRequest, InlayHintRequest, InlayHintResolveRequest, WillRenameFiles, WorkspaceSymbolRequest, - }, - CodeAction, CodeActionContext, CodeActionOrCommand, CodeActionParams, CodeActionResponse, - CompletionParams, DidChangeTextDocumentParams, DidOpenTextDocumentParams, - DidSaveTextDocumentParams, DocumentFormattingParams, FileRename, FormattingOptions, - GotoDefinitionParams, Hover, HoverParams, InlayHint, InlayHintLabel, InlayHintParams, - PartialResultParams, Position, Range, RenameFilesParams, TextDocumentContentChangeEvent, - TextDocumentIdentifier, TextDocumentItem, TextDocumentPositionParams, Url, - VersionedTextDocumentIdentifier, WorkDoneProgressParams, + }, CodeActionContext, CodeActionParams, + CompletionParams, DidOpenTextDocumentParams, DocumentFormattingParams, FileRename, FormattingOptions, + GotoDefinitionParams, HoverParams, InlayHint, InlayHintLabel, InlayHintParams, + PartialResultParams, Position, Range, RenameFilesParams, TextDocumentItem, TextDocumentPositionParams, WorkDoneProgressParams, }; -use paths::Utf8PathBuf; + use rust_analyzer::lsp::ext::{OnEnter, Runnables, RunnablesParams, UnindexedProject}; use serde_json::json; use stdx::format_to_acc; -use support::Server; + use test_utils::skip_slow_tests; use testdir::TestDir; -use tracing_subscriber::fmt::TestWriter; + use crate::support::{project, Project}; @@ -1378,1027 +1375,3 @@ version = "0.0.0" server.request::(Default::default(), json!([])); } - -enum QueryType { - AssistEmitMustUse, - /// A query whose config key is a part of the global configs, so that - /// testing for changes to this config means testing if global changes - /// take affect. - GlobalHover, -} - -struct RatomlTest { - urls: Vec, - server: Server, - tmp_path: Utf8PathBuf, - user_config_dir: Utf8PathBuf, -} - -impl RatomlTest { - const EMIT_MUST_USE: &'static str = r#"assist.emitMustUse = true"#; - const EMIT_MUST_NOT_USE: &'static str = r#"assist.emitMustUse = false"#; - const EMIT_MUST_USE_SNIPPET: &'static str = r#" - -impl Value { - #[must_use] - fn as_text(&self) -> Option<&String> { - if let Self::Text(v) = self { - Some(v) - } else { - None - } - } -}"#; - - const GLOBAL_TRAIT_ASSOC_ITEMS_ZERO: &'static str = r#"hover.show.traitAssocItems = 0"#; - const GLOBAL_TRAIT_ASSOC_ITEMS_SNIPPET: &'static str = r#" -```rust -p1 -``` - -```rust -trait RandomTrait { - type B; - fn abc() -> i32; - fn def() -> i64; -} -```"#; - - fn new( - fixtures: Vec<&str>, - roots: Vec<&str>, - client_config: Option, - ) -> Self { - // setup(); - let tmp_dir = TestDir::new(); - let tmp_path = tmp_dir.path().to_owned(); - - let full_fixture = fixtures.join("\n"); - - let user_cnf_dir = TestDir::new(); - let user_config_dir = user_cnf_dir.path().to_owned(); - - let mut project = - Project::with_fixture(&full_fixture).tmp_dir(tmp_dir).user_config_dir(user_cnf_dir); - - for root in roots { - project = project.root(root); - } - - if let Some(client_config) = client_config { - project = project.with_config(client_config); - } - - let server = project.server().wait_until_workspace_is_loaded(); - - let mut case = Self { urls: vec![], server, tmp_path, user_config_dir }; - let urls = fixtures.iter().map(|fixture| case.fixture_path(fixture)).collect::>(); - case.urls = urls; - case - } - - fn fixture_path(&self, fixture: &str) -> Url { - let mut lines = fixture.trim().split('\n'); - - let mut path = - lines.next().expect("All files in a fixture are expected to have at least one line."); - - if path.starts_with("//- minicore") { - path = lines.next().expect("A minicore line must be followed by a path.") - } - - path = path.strip_prefix("//- ").expect("Path must be preceded by a //- prefix "); - - let spl = path[1..].split('/'); - let mut path = self.tmp_path.clone(); - - let mut spl = spl.into_iter(); - if let Some(first) = spl.next() { - if first == "$$CONFIG_DIR$$" { - path = self.user_config_dir.clone(); - } else { - path = path.join(first); - } - } - for piece in spl { - path = path.join(piece); - } - - Url::parse( - format!( - "file://{}", - path.into_string().to_owned().replace("C:\\", "/c:/").replace('\\', "/") - ) - .as_str(), - ) - .unwrap() - } - - fn create(&mut self, fixture_path: &str, text: String) { - let url = self.fixture_path(fixture_path); - - self.server.notification::(DidOpenTextDocumentParams { - text_document: TextDocumentItem { - uri: url.clone(), - language_id: "rust".to_owned(), - version: 0, - text: String::new(), - }, - }); - - self.server.notification::(DidChangeTextDocumentParams { - text_document: VersionedTextDocumentIdentifier { uri: url, version: 0 }, - content_changes: vec![TextDocumentContentChangeEvent { - range: None, - range_length: None, - text, - }], - }); - } - - fn delete(&mut self, file_idx: usize) { - self.server.notification::(DidOpenTextDocumentParams { - text_document: TextDocumentItem { - uri: self.urls[file_idx].clone(), - language_id: "rust".to_owned(), - version: 0, - text: "".to_owned(), - }, - }); - - // See if deleting ratoml file will make the config of interest to return to its default value. - self.server.notification::(DidSaveTextDocumentParams { - text_document: TextDocumentIdentifier { uri: self.urls[file_idx].clone() }, - text: Some("".to_owned()), - }); - } - - fn edit(&mut self, file_idx: usize, text: String) { - self.server.notification::(DidOpenTextDocumentParams { - text_document: TextDocumentItem { - uri: self.urls[file_idx].clone(), - language_id: "rust".to_owned(), - version: 0, - text: String::new(), - }, - }); - - self.server.notification::(DidChangeTextDocumentParams { - text_document: VersionedTextDocumentIdentifier { - uri: self.urls[file_idx].clone(), - version: 0, - }, - content_changes: vec![TextDocumentContentChangeEvent { - range: None, - range_length: None, - text, - }], - }); - } - - fn query(&self, query: QueryType, source_file_idx: usize) -> bool { - match query { - QueryType::AssistEmitMustUse => { - let res = self.server.send_request::(CodeActionParams { - text_document: TextDocumentIdentifier { - uri: self.urls[source_file_idx].clone(), - }, - range: lsp_types::Range { - start: Position::new(2, 13), - end: Position::new(2, 15), - }, - context: CodeActionContext { - diagnostics: vec![], - only: None, - trigger_kind: None, - }, - work_done_progress_params: WorkDoneProgressParams { work_done_token: None }, - partial_result_params: lsp_types::PartialResultParams { - partial_result_token: None, - }, - }); - - let res = serde_json::de::from_str::(res.to_string().as_str()) - .unwrap(); - - // The difference setting the new config key will cause can be seen in the lower layers of this nested response - // so here are some ugly unwraps and other obscure stuff. - let ca: CodeAction = res - .into_iter() - .find_map(|it| { - if let CodeActionOrCommand::CodeAction(ca) = it { - if ca.title.as_str() == "Generate an `as_` method for this enum variant" - { - return Some(ca); - } - } - - None - }) - .unwrap(); - - if let lsp_types::DocumentChanges::Edits(edits) = - ca.edit.unwrap().document_changes.unwrap() - { - if let lsp_types::OneOf::Left(l) = &edits[0].edits[0] { - return l.new_text.as_str() == RatomlTest::EMIT_MUST_USE_SNIPPET; - } - } - } - QueryType::GlobalHover => { - let res = self.server.send_request::(HoverParams { - work_done_progress_params: WorkDoneProgressParams { work_done_token: None }, - text_document_position_params: TextDocumentPositionParams { - text_document: TextDocumentIdentifier { - uri: self.urls[source_file_idx].clone(), - }, - position: Position::new(7, 18), - }, - }); - let res = serde_json::de::from_str::(res.to_string().as_str()).unwrap(); - assert!(matches!(res.contents, lsp_types::HoverContents::Markup(_))); - if let lsp_types::HoverContents::Markup(m) = res.contents { - return m.value == RatomlTest::GLOBAL_TRAIT_ASSOC_ITEMS_SNIPPET; - } - } - } - - panic!() - } -} - -static INIT: Once = Once::new(); - -fn setup() { - INIT.call_once(|| { - let trc = rust_analyzer::tracing::Config { - writer: TestWriter::default(), - // Deliberately enable all `error` logs if the user has not set RA_LOG, as there is usually - // useful information in there for debugging. - filter: std::env::var("RA_LOG").ok().unwrap_or_else(|| "error".to_owned()), - chalk_filter: std::env::var("CHALK_DEBUG").ok(), - profile_filter: std::env::var("RA_PROFILE").ok(), - }; - - trc.init().unwrap(); - }); -} - -// /// Check if we are listening for changes in user's config file ( e.g on Linux `~/.config/rust-analyzer/.rust-analyzer.toml`) -// #[test] -// #[cfg(target_os = "windows")] -// fn listen_to_user_config_scenario_windows() { -// todo!() -// } - -// #[test] -// #[cfg(target_os = "linux")] -// fn listen_to_user_config_scenario_linux() { -// todo!() -// } - -// #[test] -// #[cfg(target_os = "macos")] -// fn listen_to_user_config_scenario_macos() { -// todo!() -// } - -/// Check if made changes have had any effect on -/// the client config. -#[test] -fn ratoml_client_config_basic() { - let server = RatomlTest::new( - vec![ - r#" -//- /p1/Cargo.toml -[package] -name = "p1" -version = "0.1.0" -edition = "2021" -"#, - r#"//- /p1/src/lib.rs -enum Value { - Number(i32), - Text(String), -}"#, - ], - vec!["p1"], - Some(json!({ - "assist" : { - "emitMustUse" : true - } - })), - ); - - assert!(server.query(QueryType::AssistEmitMustUse, 1)); -} - -/// Checks if client config can be modified. -/// FIXME @alibektas : This test is atm not valid. -/// Asking for client config from the client is a 2 way communication -/// which we cannot imitate with the current slow-tests infrastructure. -/// See rust-analyzer::handlers::notifications#197 -// #[test] -// fn client_config_update() { -// setup(); - -// let server = RatomlTest::new( -// vec![ -// r#" -// //- /p1/Cargo.toml -// [package] -// name = "p1" -// version = "0.1.0" -// edition = "2021" -// "#, -// r#" -// //- /p1/src/lib.rs -// enum Value { -// Number(i32), -// Text(String), -// }"#, -// ], -// vec!["p1"], -// None, -// ); - -// assert!(!server.query(QueryType::AssistEmitMustUse, 1)); - -// // a.notification::(DidChangeConfigurationParams { -// // settings: json!({ -// // "assists" : { -// // "emitMustUse" : true -// // } -// // }), -// // }); - -// assert!(server.query(QueryType::AssistEmitMustUse, 1)); -// } - -// #[test] -// fn ratoml_create_ratoml_basic() { -// let server = RatomlTest::new( -// vec![ -// r#" -// //- /p1/Cargo.toml -// [package] -// name = "p1" -// version = "0.1.0" -// edition = "2021" -// "#, -// r#" -// //- /p1/rust-analyzer.toml -// assist.emitMustUse = true -// "#, -// r#" -// //- /p1/src/lib.rs -// enum Value { -// Number(i32), -// Text(String), -// } -// "#, -// ], -// vec!["p1"], -// None, -// ); - -// assert!(server.query(QueryType::AssistEmitMustUse, 2)); -// } - -#[test] -fn ratoml_user_config_detected() { - let server = RatomlTest::new( - vec![ - r#" -//- /$$CONFIG_DIR$$/rust-analyzer/rust-analyzer.toml -assist.emitMustUse = true -"#, - r#" -//- /p1/Cargo.toml -[package] -name = "p1" -version = "0.1.0" -edition = "2021" -"#, - r#"//- /p1/src/lib.rs -enum Value { - Number(i32), - Text(String), -}"#, - ], - vec!["p1"], - None, - ); - - assert!(server.query(QueryType::AssistEmitMustUse, 2)); -} - -#[test] -fn ratoml_create_user_config() { - setup(); - let mut server = RatomlTest::new( - vec![ - r#" -//- /p1/Cargo.toml -[package] -name = "p1" -version = "0.1.0" -edition = "2021" -"#, - r#" -//- /p1/src/lib.rs -enum Value { - Number(i32), - Text(String), -}"#, - ], - vec!["p1"], - None, - ); - - assert!(!server.query(QueryType::AssistEmitMustUse, 1)); - server.create( - "//- /$$CONFIG_DIR$$/rust-analyzer/rust-analyzer.toml", - RatomlTest::EMIT_MUST_USE.to_owned(), - ); - assert!(server.query(QueryType::AssistEmitMustUse, 1)); -} - -#[test] -fn ratoml_modify_user_config() { - let mut server = RatomlTest::new( - vec![ - r#" -//- /p1/Cargo.toml -[package] -name = "p1" -version = "0.1.0" -edition = "2021""#, - r#" -//- /p1/src/lib.rs -enum Value { - Number(i32), - Text(String), -}"#, - r#" -//- /$$CONFIG_DIR$$/rust-analyzer/rust-analyzer.toml -assist.emitMustUse = true"#, - ], - vec!["p1"], - None, - ); - - assert!(server.query(QueryType::AssistEmitMustUse, 1)); - server.edit(2, String::new()); - assert!(!server.query(QueryType::AssistEmitMustUse, 1)); -} - -#[test] -fn ratoml_delete_user_config() { - let mut server = RatomlTest::new( - vec![ - r#" -//- /p1/Cargo.toml -[package] -name = "p1" -version = "0.1.0" -edition = "2021""#, - r#" -//- /p1/src/lib.rs -enum Value { - Number(i32), - Text(String), -}"#, - r#" -//- /$$CONFIG_DIR$$/rust-analyzer/rust-analyzer.toml -assist.emitMustUse = true"#, - ], - vec!["p1"], - None, - ); - - assert!(server.query(QueryType::AssistEmitMustUse, 1)); - server.delete(2); - assert!(!server.query(QueryType::AssistEmitMustUse, 1)); -} -// #[test] -// fn delete_user_config() { -// todo!() -// } - -// #[test] -// fn modify_client_config() { -// todo!() -// } - -#[test] -fn ratoml_inherit_config_from_ws_root() { - let server = RatomlTest::new( - vec![ - r#" -//- /p1/Cargo.toml -workspace = { members = ["p2"] } -[package] -name = "p1" -version = "0.1.0" -edition = "2021" -"#, - r#" -//- /p1/rust-analyzer.toml -assist.emitMustUse = true -"#, - r#" -//- /p1/p2/Cargo.toml -[package] -name = "p2" -version = "0.1.0" -edition = "2021" -"#, - r#" -//- /p1/p2/src/lib.rs -enum Value { - Number(i32), - Text(String), -}"#, - r#" -//- /p1/src/lib.rs -pub fn add(left: usize, right: usize) -> usize { - left + right -} -"#, - ], - vec!["p1"], - None, - ); - - assert!(server.query(QueryType::AssistEmitMustUse, 3)); -} - -#[test] -fn ratoml_modify_ratoml_at_ws_root() { - let mut server = RatomlTest::new( - vec![ - r#" -//- /p1/Cargo.toml -workspace = { members = ["p2"] } -[package] -name = "p1" -version = "0.1.0" -edition = "2021" -"#, - r#" -//- /p1/rust-analyzer.toml -assist.emitMustUse = false -"#, - r#" -//- /p1/p2/Cargo.toml -[package] -name = "p2" -version = "0.1.0" -edition = "2021" -"#, - r#" -//- /p1/p2/src/lib.rs -enum Value { - Number(i32), - Text(String), -}"#, - r#" -//- /p1/src/lib.rs -pub fn add(left: usize, right: usize) -> usize { - left + right -} -"#, - ], - vec!["p1"], - None, - ); - - assert!(!server.query(QueryType::AssistEmitMustUse, 3)); - server.edit(1, "assist.emitMustUse = true".to_owned()); - assert!(server.query(QueryType::AssistEmitMustUse, 3)); -} - -#[test] -fn ratoml_delete_ratoml_at_ws_root() { - let mut server = RatomlTest::new( - vec![ - r#" -//- /p1/Cargo.toml -workspace = { members = ["p2"] } -[package] -name = "p1" -version = "0.1.0" -edition = "2021" -"#, - r#" -//- /p1/rust-analyzer.toml -assist.emitMustUse = true -"#, - r#" -//- /p1/p2/Cargo.toml -[package] -name = "p2" -version = "0.1.0" -edition = "2021" -"#, - r#" -//- /p1/p2/src/lib.rs -enum Value { - Number(i32), - Text(String), -}"#, - r#" -//- /p1/src/lib.rs -pub fn add(left: usize, right: usize) -> usize { - left + right -} -"#, - ], - vec!["p1"], - None, - ); - - assert!(server.query(QueryType::AssistEmitMustUse, 3)); - server.delete(1); - assert!(!server.query(QueryType::AssistEmitMustUse, 3)); -} - -#[test] -fn ratoml_add_immediate_child_to_ws_root() { - let mut server = RatomlTest::new( - vec![ - r#" -//- /p1/Cargo.toml -workspace = { members = ["p2"] } -[package] -name = "p1" -version = "0.1.0" -edition = "2021" -"#, - r#" -//- /p1/rust-analyzer.toml -assist.emitMustUse = true -"#, - r#" -//- /p1/p2/Cargo.toml -[package] -name = "p2" -version = "0.1.0" -edition = "2021" -"#, - r#" -//- /p1/p2/src/lib.rs -enum Value { - Number(i32), - Text(String), -}"#, - r#" -//- /p1/src/lib.rs -pub fn add(left: usize, right: usize) -> usize { - left + right -} -"#, - ], - vec!["p1"], - None, - ); - - assert!(server.query(QueryType::AssistEmitMustUse, 3)); - server.create("//- /p1/p2/rust-analyzer.toml", RatomlTest::EMIT_MUST_NOT_USE.to_owned()); - assert!(!server.query(QueryType::AssistEmitMustUse, 3)); -} - -#[test] -fn ratoml_rm_ws_root_ratoml_child_has_client_as_parent_now() { - let mut server = RatomlTest::new( - vec![ - r#" -//- /p1/Cargo.toml -workspace = { members = ["p2"] } -[package] -name = "p1" -version = "0.1.0" -edition = "2021" -"#, - r#" -//- /p1/rust-analyzer.toml -assist.emitMustUse = true -"#, - r#" -//- /p1/p2/Cargo.toml -[package] -name = "p2" -version = "0.1.0" -edition = "2021" -"#, - r#" -//- /p1/p2/src/lib.rs -enum Value { - Number(i32), - Text(String), -}"#, - r#" -//- /p1/src/lib.rs -pub fn add(left: usize, right: usize) -> usize { - left + right -} -"#, - ], - vec!["p1"], - None, - ); - - assert!(server.query(QueryType::AssistEmitMustUse, 3)); - server.delete(1); - assert!(!server.query(QueryType::AssistEmitMustUse, 3)); -} - -#[test] -fn ratoml_crates_both_roots() { - let server = RatomlTest::new( - vec![ - r#" -//- /p1/Cargo.toml -workspace = { members = ["p2"] } -[package] -name = "p1" -version = "0.1.0" -edition = "2021" -"#, - r#" -//- /p1/rust-analyzer.toml -assist.emitMustUse = true -"#, - r#" -//- /p1/p2/Cargo.toml -[package] -name = "p2" -version = "0.1.0" -edition = "2021" -"#, - r#" -//- /p1/p2/src/lib.rs -enum Value { - Number(i32), - Text(String), -}"#, - r#" -//- /p1/src/lib.rs -enum Value { - Number(i32), - Text(String), -}"#, - ], - vec!["p1", "p2"], - None, - ); - - assert!(server.query(QueryType::AssistEmitMustUse, 3)); - assert!(server.query(QueryType::AssistEmitMustUse, 4)); -} - -#[test] -fn ratoml_multiple_ratoml_in_single_source_root() { - let server = RatomlTest::new( - vec![ - r#" - //- /p1/Cargo.toml - [package] - name = "p1" - version = "0.1.0" - edition = "2021" - "#, - r#" - //- /p1/rust-analyzer.toml - assist.emitMustUse = true - "#, - r#" - //- /p1/src/rust-analyzer.toml - assist.emitMustUse = false - "#, - r#" - //- /p1/src/lib.rs - enum Value { - Number(i32), - Text(String), - } - "#, - ], - vec!["p1"], - None, - ); - - assert!(server.query(QueryType::AssistEmitMustUse, 3)); - - let server = RatomlTest::new( - vec![ - r#" -//- /p1/Cargo.toml -[package] -name = "p1" -version = "0.1.0" -edition = "2021" -"#, - r#" -//- /p1/src/rust-analyzer.toml -assist.emitMustUse = false -"#, - r#" -//- /p1/rust-analyzer.toml -assist.emitMustUse = true -"#, - r#" -//- /p1/src/lib.rs -enum Value { - Number(i32), - Text(String), -} -"#, - ], - vec!["p1"], - None, - ); - - assert!(server.query(QueryType::AssistEmitMustUse, 3)); -} - -/// If a root is non-local, so we cannot find what its parent is -/// in our `config.local_root_parent_map`. So if any config should -/// apply, it must be looked for starting from the client level. -/// FIXME @alibektas : "locality" is according to ra that, which is simply in the file system. -/// This doesn't really help us with what we want to achieve here. -// #[test] -// fn ratoml_non_local_crates_start_inheriting_from_client() { -// let server = RatomlTest::new( -// vec![ -// r#" -// //- /p1/Cargo.toml -// [package] -// name = "p1" -// version = "0.1.0" -// edition = "2021" - -// [dependencies] -// p2 = { path = "../p2" } -// #, -// r#" -// //- /p1/src/lib.rs -// enum Value { -// Number(i32), -// Text(String), -// } - -// use p2; - -// pub fn add(left: usize, right: usize) -> usize { -// p2::add(left, right) -// } - -// #[cfg(test)] -// mod tests { -// use super::*; - -// #[test] -// fn it_works() { -// let result = add(2, 2); -// assert_eq!(result, 4); -// } -// }"#, -// r#" -// //- /p2/Cargo.toml -// [package] -// name = "p2" -// version = "0.1.0" -// edition = "2021" - -// # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -// [dependencies] -// "#, -// r#" -// //- /p2/rust-analyzer.toml -// # DEF -// assist.emitMustUse = true -// "#, -// r#" -// //- /p2/src/lib.rs -// enum Value { -// Number(i32), -// Text(String), -// }"#, -// ], -// vec!["p1", "p2"], -// None, -// ); - -// assert!(!server.query(QueryType::AssistEmitMustUse, 5)); -// } - -/// Having a ratoml file at the root of a project enables -/// configuring global level configurations as well. -#[test] -fn ratoml_in_root_is_global() { - let server = RatomlTest::new( - vec![ - r#" -//- /p1/Cargo.toml -[package] -name = "p1" -version = "0.1.0" -edition = "2021" - "#, - r#" -//- /rust-analyzer.toml -hover.show.traitAssocItems = 4 - "#, - r#" -//- /p1/src/lib.rs -trait RandomTrait { - type B; - fn abc() -> i32; - fn def() -> i64; -} - -fn main() { - let a = RandomTrait; -}"#, - ], - vec![], - None, - ); - - server.query(QueryType::GlobalHover, 2); -} - -#[test] -fn ratoml_root_is_updateable() { - let mut server = RatomlTest::new( - vec![ - r#" -//- /p1/Cargo.toml -[package] -name = "p1" -version = "0.1.0" -edition = "2021" - "#, - r#" -//- /rust-analyzer.toml -hover.show.traitAssocItems = 4 - "#, - r#" -//- /p1/src/lib.rs -trait RandomTrait { - type B; - fn abc() -> i32; - fn def() -> i64; -} - -fn main() { - let a = RandomTrait; -}"#, - ], - vec![], - None, - ); - - assert!(server.query(QueryType::GlobalHover, 2)); - server.edit(1, RatomlTest::GLOBAL_TRAIT_ASSOC_ITEMS_ZERO.to_owned()); - assert!(!server.query(QueryType::GlobalHover, 2)); -} - -#[test] -fn ratoml_root_is_deletable() { - let mut server = RatomlTest::new( - vec![ - r#" -//- /p1/Cargo.toml -[package] -name = "p1" -version = "0.1.0" -edition = "2021" - "#, - r#" -//- /rust-analyzer.toml -hover.show.traitAssocItems = 4 - "#, - r#" -//- /p1/src/lib.rs -trait RandomTrait { - type B; - fn abc() -> i32; - fn def() -> i64; -} - -fn main() { - let a = RandomTrait; -}"#, - ], - vec![], - None, - ); - - assert!(server.query(QueryType::GlobalHover, 2)); - server.delete(1); - assert!(!server.query(QueryType::GlobalHover, 2)); -} diff --git a/crates/rust-analyzer/tests/slow-tests/ratoml.rs b/crates/rust-analyzer/tests/slow-tests/ratoml.rs new file mode 100644 index 000000000000..739da9c998d5 --- /dev/null +++ b/crates/rust-analyzer/tests/slow-tests/ratoml.rs @@ -0,0 +1,1019 @@ +use crate::support::{Project, Server}; +use crate::testdir::TestDir; +use lsp_types::{ + notification::{DidChangeTextDocument, DidOpenTextDocument, DidSaveTextDocument}, + request::{CodeActionRequest, HoverRequest}, + CodeAction, CodeActionContext, CodeActionOrCommand, CodeActionParams, CodeActionResponse, + DidChangeTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, Hover, + HoverParams, Position, TextDocumentContentChangeEvent, TextDocumentIdentifier, + TextDocumentItem, TextDocumentPositionParams, Url, VersionedTextDocumentIdentifier, + WorkDoneProgressParams, +}; +use paths::Utf8PathBuf; + +use serde_json::json; + +enum QueryType { + AssistEmitMustUse, + /// A query whose config key is a part of the global configs, so that + /// testing for changes to this config means testing if global changes + /// take affect. + GlobalHover, +} + +struct RatomlTest { + urls: Vec, + server: Server, + tmp_path: Utf8PathBuf, + user_config_dir: Utf8PathBuf, +} + +impl RatomlTest { + const EMIT_MUST_USE: &'static str = r#"assist.emitMustUse = true"#; + const EMIT_MUST_NOT_USE: &'static str = r#"assist.emitMustUse = false"#; + const EMIT_MUST_USE_SNIPPET: &'static str = r#" + +impl Value { + #[must_use] + fn as_text(&self) -> Option<&String> { + if let Self::Text(v) = self { + Some(v) + } else { + None + } + } +}"#; + + const GLOBAL_TRAIT_ASSOC_ITEMS_ZERO: &'static str = r#"hover.show.traitAssocItems = 0"#; + const GLOBAL_TRAIT_ASSOC_ITEMS_SNIPPET: &'static str = r#" +```rust +p1 +``` + +```rust +trait RandomTrait { + type B; + fn abc() -> i32; + fn def() -> i64; +} +```"#; + + fn new( + fixtures: Vec<&str>, + roots: Vec<&str>, + client_config: Option, + ) -> Self { + let tmp_dir = TestDir::new(); + let tmp_path = tmp_dir.path().to_owned(); + + let full_fixture = fixtures.join("\n"); + + let user_cnf_dir = TestDir::new(); + let user_config_dir = user_cnf_dir.path().to_owned(); + + let mut project = + Project::with_fixture(&full_fixture).tmp_dir(tmp_dir).user_config_dir(user_cnf_dir); + + for root in roots { + project = project.root(root); + } + + if let Some(client_config) = client_config { + project = project.with_config(client_config); + } + + let server = project.server().wait_until_workspace_is_loaded(); + + let mut case = Self { urls: vec![], server, tmp_path, user_config_dir }; + let urls = fixtures.iter().map(|fixture| case.fixture_path(fixture)).collect::>(); + case.urls = urls; + case + } + + fn fixture_path(&self, fixture: &str) -> Url { + let mut lines = fixture.trim().split('\n'); + + let mut path = + lines.next().expect("All files in a fixture are expected to have at least one line."); + + if path.starts_with("//- minicore") { + path = lines.next().expect("A minicore line must be followed by a path.") + } + + path = path.strip_prefix("//- ").expect("Path must be preceded by a //- prefix "); + + let spl = path[1..].split('/'); + let mut path = self.tmp_path.clone(); + + let mut spl = spl.into_iter(); + if let Some(first) = spl.next() { + if first == "$$CONFIG_DIR$$" { + path = self.user_config_dir.clone(); + } else { + path = path.join(first); + } + } + for piece in spl { + path = path.join(piece); + } + + Url::parse( + format!( + "file://{}", + path.into_string().to_owned().replace("C:\\", "/c:/").replace('\\', "/") + ) + .as_str(), + ) + .unwrap() + } + + fn create(&mut self, fixture_path: &str, text: String) { + let url = self.fixture_path(fixture_path); + + self.server.notification::(DidOpenTextDocumentParams { + text_document: TextDocumentItem { + uri: url.clone(), + language_id: "rust".to_owned(), + version: 0, + text: String::new(), + }, + }); + + self.server.notification::(DidChangeTextDocumentParams { + text_document: VersionedTextDocumentIdentifier { uri: url, version: 0 }, + content_changes: vec![TextDocumentContentChangeEvent { + range: None, + range_length: None, + text, + }], + }); + } + + fn delete(&mut self, file_idx: usize) { + self.server.notification::(DidOpenTextDocumentParams { + text_document: TextDocumentItem { + uri: self.urls[file_idx].clone(), + language_id: "rust".to_owned(), + version: 0, + text: "".to_owned(), + }, + }); + + // See if deleting ratoml file will make the config of interest to return to its default value. + self.server.notification::(DidSaveTextDocumentParams { + text_document: TextDocumentIdentifier { uri: self.urls[file_idx].clone() }, + text: Some("".to_owned()), + }); + } + + fn edit(&mut self, file_idx: usize, text: String) { + self.server.notification::(DidOpenTextDocumentParams { + text_document: TextDocumentItem { + uri: self.urls[file_idx].clone(), + language_id: "rust".to_owned(), + version: 0, + text: String::new(), + }, + }); + + self.server.notification::(DidChangeTextDocumentParams { + text_document: VersionedTextDocumentIdentifier { + uri: self.urls[file_idx].clone(), + version: 0, + }, + content_changes: vec![TextDocumentContentChangeEvent { + range: None, + range_length: None, + text, + }], + }); + } + + fn query(&self, query: QueryType, source_file_idx: usize) -> bool { + match query { + QueryType::AssistEmitMustUse => { + let res = self.server.send_request::(CodeActionParams { + text_document: TextDocumentIdentifier { + uri: self.urls[source_file_idx].clone(), + }, + range: lsp_types::Range { + start: Position::new(2, 13), + end: Position::new(2, 15), + }, + context: CodeActionContext { + diagnostics: vec![], + only: None, + trigger_kind: None, + }, + work_done_progress_params: WorkDoneProgressParams { work_done_token: None }, + partial_result_params: lsp_types::PartialResultParams { + partial_result_token: None, + }, + }); + + let res = serde_json::de::from_str::(res.to_string().as_str()) + .unwrap(); + + // The difference setting the new config key will cause can be seen in the lower layers of this nested response + // so here are some ugly unwraps and other obscure stuff. + let ca: CodeAction = res + .into_iter() + .find_map(|it| { + if let CodeActionOrCommand::CodeAction(ca) = it { + if ca.title.as_str() == "Generate an `as_` method for this enum variant" + { + return Some(ca); + } + } + + None + }) + .unwrap(); + + if let lsp_types::DocumentChanges::Edits(edits) = + ca.edit.unwrap().document_changes.unwrap() + { + if let lsp_types::OneOf::Left(l) = &edits[0].edits[0] { + return l.new_text.as_str() == RatomlTest::EMIT_MUST_USE_SNIPPET; + } + } + } + QueryType::GlobalHover => { + let res = self.server.send_request::(HoverParams { + work_done_progress_params: WorkDoneProgressParams { work_done_token: None }, + text_document_position_params: TextDocumentPositionParams { + text_document: TextDocumentIdentifier { + uri: self.urls[source_file_idx].clone(), + }, + position: Position::new(7, 18), + }, + }); + let res = serde_json::de::from_str::(res.to_string().as_str()).unwrap(); + assert!(matches!(res.contents, lsp_types::HoverContents::Markup(_))); + if let lsp_types::HoverContents::Markup(m) = res.contents { + return m.value == RatomlTest::GLOBAL_TRAIT_ASSOC_ITEMS_SNIPPET; + } + } + } + + panic!() + } +} + +// /// Check if we are listening for changes in user's config file ( e.g on Linux `~/.config/rust-analyzer/.rust-analyzer.toml`) +// #[test] +// #[cfg(target_os = "windows")] +// fn listen_to_user_config_scenario_windows() { +// todo!() +// } + +// #[test] +// #[cfg(target_os = "linux")] +// fn listen_to_user_config_scenario_linux() { +// todo!() +// } + +// #[test] +// #[cfg(target_os = "macos")] +// fn listen_to_user_config_scenario_macos() { +// todo!() +// } + +/// Check if made changes have had any effect on +/// the client config. +#[test] +fn ratoml_client_config_basic() { + let server = RatomlTest::new( + vec![ + r#" +//- /p1/Cargo.toml +[package] +name = "p1" +version = "0.1.0" +edition = "2021" +"#, + r#"//- /p1/src/lib.rs +enum Value { + Number(i32), + Text(String), +}"#, + ], + vec!["p1"], + Some(json!({ + "assist" : { + "emitMustUse" : true + } + })), + ); + + assert!(server.query(QueryType::AssistEmitMustUse, 1)); +} + +/// Checks if client config can be modified. +/// FIXME @alibektas : This test is atm not valid. +/// Asking for client config from the client is a 2 way communication +/// which we cannot imitate with the current slow-tests infrastructure. +/// See rust-analyzer::handlers::notifications#197 +// #[test] +// fn client_config_update() { +// setup(); + +// let server = RatomlTest::new( +// vec![ +// r#" +// //- /p1/Cargo.toml +// [package] +// name = "p1" +// version = "0.1.0" +// edition = "2021" +// "#, +// r#" +// //- /p1/src/lib.rs +// enum Value { +// Number(i32), +// Text(String), +// }"#, +// ], +// vec!["p1"], +// None, +// ); + +// assert!(!server.query(QueryType::AssistEmitMustUse, 1)); + +// // a.notification::(DidChangeConfigurationParams { +// // settings: json!({ +// // "assists" : { +// // "emitMustUse" : true +// // } +// // }), +// // }); + +// assert!(server.query(QueryType::AssistEmitMustUse, 1)); +// } + +// #[test] +// fn ratoml_create_ratoml_basic() { +// let server = RatomlTest::new( +// vec![ +// r#" +// //- /p1/Cargo.toml +// [package] +// name = "p1" +// version = "0.1.0" +// edition = "2021" +// "#, +// r#" +// //- /p1/rust-analyzer.toml +// assist.emitMustUse = true +// "#, +// r#" +// //- /p1/src/lib.rs +// enum Value { +// Number(i32), +// Text(String), +// } +// "#, +// ], +// vec!["p1"], +// None, +// ); + +// assert!(server.query(QueryType::AssistEmitMustUse, 2)); +// } + +#[test] +fn ratoml_user_config_detected() { + let server = RatomlTest::new( + vec![ + r#" +//- /$$CONFIG_DIR$$/rust-analyzer/rust-analyzer.toml +assist.emitMustUse = true +"#, + r#" +//- /p1/Cargo.toml +[package] +name = "p1" +version = "0.1.0" +edition = "2021" +"#, + r#"//- /p1/src/lib.rs +enum Value { + Number(i32), + Text(String), +}"#, + ], + vec!["p1"], + None, + ); + + assert!(server.query(QueryType::AssistEmitMustUse, 2)); +} + +#[test] +fn ratoml_create_user_config() { + let mut server = RatomlTest::new( + vec![ + r#" +//- /p1/Cargo.toml +[package] +name = "p1" +version = "0.1.0" +edition = "2021" +"#, + r#" +//- /p1/src/lib.rs +enum Value { + Number(i32), + Text(String), +}"#, + ], + vec!["p1"], + None, + ); + + assert!(!server.query(QueryType::AssistEmitMustUse, 1)); + server.create( + "//- /$$CONFIG_DIR$$/rust-analyzer/rust-analyzer.toml", + RatomlTest::EMIT_MUST_USE.to_owned(), + ); + assert!(server.query(QueryType::AssistEmitMustUse, 1)); +} + +#[test] +fn ratoml_modify_user_config() { + let mut server = RatomlTest::new( + vec![ + r#" +//- /p1/Cargo.toml +[package] +name = "p1" +version = "0.1.0" +edition = "2021""#, + r#" +//- /p1/src/lib.rs +enum Value { + Number(i32), + Text(String), +}"#, + r#" +//- /$$CONFIG_DIR$$/rust-analyzer/rust-analyzer.toml +assist.emitMustUse = true"#, + ], + vec!["p1"], + None, + ); + + assert!(server.query(QueryType::AssistEmitMustUse, 1)); + server.edit(2, String::new()); + assert!(!server.query(QueryType::AssistEmitMustUse, 1)); +} + +#[test] +fn ratoml_delete_user_config() { + let mut server = RatomlTest::new( + vec![ + r#" +//- /p1/Cargo.toml +[package] +name = "p1" +version = "0.1.0" +edition = "2021""#, + r#" +//- /p1/src/lib.rs +enum Value { + Number(i32), + Text(String), +}"#, + r#" +//- /$$CONFIG_DIR$$/rust-analyzer/rust-analyzer.toml +assist.emitMustUse = true"#, + ], + vec!["p1"], + None, + ); + + assert!(server.query(QueryType::AssistEmitMustUse, 1)); + server.delete(2); + assert!(!server.query(QueryType::AssistEmitMustUse, 1)); +} +// #[test] +// fn delete_user_config() { +// todo!() +// } + +// #[test] +// fn modify_client_config() { +// todo!() +// } + +#[test] +fn ratoml_inherit_config_from_ws_root() { + let server = RatomlTest::new( + vec![ + r#" +//- /p1/Cargo.toml +workspace = { members = ["p2"] } +[package] +name = "p1" +version = "0.1.0" +edition = "2021" +"#, + r#" +//- /p1/rust-analyzer.toml +assist.emitMustUse = true +"#, + r#" +//- /p1/p2/Cargo.toml +[package] +name = "p2" +version = "0.1.0" +edition = "2021" +"#, + r#" +//- /p1/p2/src/lib.rs +enum Value { + Number(i32), + Text(String), +}"#, + r#" +//- /p1/src/lib.rs +pub fn add(left: usize, right: usize) -> usize { + left + right +} +"#, + ], + vec!["p1"], + None, + ); + + assert!(server.query(QueryType::AssistEmitMustUse, 3)); +} + +#[test] +fn ratoml_modify_ratoml_at_ws_root() { + let mut server = RatomlTest::new( + vec![ + r#" +//- /p1/Cargo.toml +workspace = { members = ["p2"] } +[package] +name = "p1" +version = "0.1.0" +edition = "2021" +"#, + r#" +//- /p1/rust-analyzer.toml +assist.emitMustUse = false +"#, + r#" +//- /p1/p2/Cargo.toml +[package] +name = "p2" +version = "0.1.0" +edition = "2021" +"#, + r#" +//- /p1/p2/src/lib.rs +enum Value { + Number(i32), + Text(String), +}"#, + r#" +//- /p1/src/lib.rs +pub fn add(left: usize, right: usize) -> usize { + left + right +} +"#, + ], + vec!["p1"], + None, + ); + + assert!(!server.query(QueryType::AssistEmitMustUse, 3)); + server.edit(1, "assist.emitMustUse = true".to_owned()); + assert!(server.query(QueryType::AssistEmitMustUse, 3)); +} + +#[test] +fn ratoml_delete_ratoml_at_ws_root() { + let mut server = RatomlTest::new( + vec![ + r#" +//- /p1/Cargo.toml +workspace = { members = ["p2"] } +[package] +name = "p1" +version = "0.1.0" +edition = "2021" +"#, + r#" +//- /p1/rust-analyzer.toml +assist.emitMustUse = true +"#, + r#" +//- /p1/p2/Cargo.toml +[package] +name = "p2" +version = "0.1.0" +edition = "2021" +"#, + r#" +//- /p1/p2/src/lib.rs +enum Value { + Number(i32), + Text(String), +}"#, + r#" +//- /p1/src/lib.rs +pub fn add(left: usize, right: usize) -> usize { + left + right +} +"#, + ], + vec!["p1"], + None, + ); + + assert!(server.query(QueryType::AssistEmitMustUse, 3)); + server.delete(1); + assert!(!server.query(QueryType::AssistEmitMustUse, 3)); +} + +#[test] +fn ratoml_add_immediate_child_to_ws_root() { + let mut server = RatomlTest::new( + vec![ + r#" +//- /p1/Cargo.toml +workspace = { members = ["p2"] } +[package] +name = "p1" +version = "0.1.0" +edition = "2021" +"#, + r#" +//- /p1/rust-analyzer.toml +assist.emitMustUse = true +"#, + r#" +//- /p1/p2/Cargo.toml +[package] +name = "p2" +version = "0.1.0" +edition = "2021" +"#, + r#" +//- /p1/p2/src/lib.rs +enum Value { + Number(i32), + Text(String), +}"#, + r#" +//- /p1/src/lib.rs +pub fn add(left: usize, right: usize) -> usize { + left + right +} +"#, + ], + vec!["p1"], + None, + ); + + assert!(server.query(QueryType::AssistEmitMustUse, 3)); + server.create("//- /p1/p2/rust-analyzer.toml", RatomlTest::EMIT_MUST_NOT_USE.to_owned()); + assert!(!server.query(QueryType::AssistEmitMustUse, 3)); +} + +#[test] +fn ratoml_rm_ws_root_ratoml_child_has_client_as_parent_now() { + let mut server = RatomlTest::new( + vec![ + r#" +//- /p1/Cargo.toml +workspace = { members = ["p2"] } +[package] +name = "p1" +version = "0.1.0" +edition = "2021" +"#, + r#" +//- /p1/rust-analyzer.toml +assist.emitMustUse = true +"#, + r#" +//- /p1/p2/Cargo.toml +[package] +name = "p2" +version = "0.1.0" +edition = "2021" +"#, + r#" +//- /p1/p2/src/lib.rs +enum Value { + Number(i32), + Text(String), +}"#, + r#" +//- /p1/src/lib.rs +pub fn add(left: usize, right: usize) -> usize { + left + right +} +"#, + ], + vec!["p1"], + None, + ); + + assert!(server.query(QueryType::AssistEmitMustUse, 3)); + server.delete(1); + assert!(!server.query(QueryType::AssistEmitMustUse, 3)); +} + +#[test] +fn ratoml_crates_both_roots() { + let server = RatomlTest::new( + vec![ + r#" +//- /p1/Cargo.toml +workspace = { members = ["p2"] } +[package] +name = "p1" +version = "0.1.0" +edition = "2021" +"#, + r#" +//- /p1/rust-analyzer.toml +assist.emitMustUse = true +"#, + r#" +//- /p1/p2/Cargo.toml +[package] +name = "p2" +version = "0.1.0" +edition = "2021" +"#, + r#" +//- /p1/p2/src/lib.rs +enum Value { + Number(i32), + Text(String), +}"#, + r#" +//- /p1/src/lib.rs +enum Value { + Number(i32), + Text(String), +}"#, + ], + vec!["p1", "p2"], + None, + ); + + assert!(server.query(QueryType::AssistEmitMustUse, 3)); + assert!(server.query(QueryType::AssistEmitMustUse, 4)); +} + +#[test] +fn ratoml_multiple_ratoml_in_single_source_root() { + let server = RatomlTest::new( + vec![ + r#" + //- /p1/Cargo.toml + [package] + name = "p1" + version = "0.1.0" + edition = "2021" + "#, + r#" + //- /p1/rust-analyzer.toml + assist.emitMustUse = true + "#, + r#" + //- /p1/src/rust-analyzer.toml + assist.emitMustUse = false + "#, + r#" + //- /p1/src/lib.rs + enum Value { + Number(i32), + Text(String), + } + "#, + ], + vec!["p1"], + None, + ); + + assert!(server.query(QueryType::AssistEmitMustUse, 3)); + + let server = RatomlTest::new( + vec![ + r#" +//- /p1/Cargo.toml +[package] +name = "p1" +version = "0.1.0" +edition = "2021" +"#, + r#" +//- /p1/src/rust-analyzer.toml +assist.emitMustUse = false +"#, + r#" +//- /p1/rust-analyzer.toml +assist.emitMustUse = true +"#, + r#" +//- /p1/src/lib.rs +enum Value { + Number(i32), + Text(String), +} +"#, + ], + vec!["p1"], + None, + ); + + assert!(server.query(QueryType::AssistEmitMustUse, 3)); +} + +/// If a root is non-local, so we cannot find what its parent is +/// in our `config.local_root_parent_map`. So if any config should +/// apply, it must be looked for starting from the client level. +/// FIXME @alibektas : "locality" is according to ra that, which is simply in the file system. +/// This doesn't really help us with what we want to achieve here. +// #[test] +// fn ratoml_non_local_crates_start_inheriting_from_client() { +// let server = RatomlTest::new( +// vec![ +// r#" +// //- /p1/Cargo.toml +// [package] +// name = "p1" +// version = "0.1.0" +// edition = "2021" + +// [dependencies] +// p2 = { path = "../p2" } +// #, +// r#" +// //- /p1/src/lib.rs +// enum Value { +// Number(i32), +// Text(String), +// } + +// use p2; + +// pub fn add(left: usize, right: usize) -> usize { +// p2::add(left, right) +// } + +// #[cfg(test)] +// mod tests { +// use super::*; + +// #[test] +// fn it_works() { +// let result = add(2, 2); +// assert_eq!(result, 4); +// } +// }"#, +// r#" +// //- /p2/Cargo.toml +// [package] +// name = "p2" +// version = "0.1.0" +// edition = "2021" + +// # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +// [dependencies] +// "#, +// r#" +// //- /p2/rust-analyzer.toml +// # DEF +// assist.emitMustUse = true +// "#, +// r#" +// //- /p2/src/lib.rs +// enum Value { +// Number(i32), +// Text(String), +// }"#, +// ], +// vec!["p1", "p2"], +// None, +// ); + +// assert!(!server.query(QueryType::AssistEmitMustUse, 5)); +// } + +/// Having a ratoml file at the root of a project enables +/// configuring global level configurations as well. +#[test] +fn ratoml_in_root_is_global() { + let server = RatomlTest::new( + vec![ + r#" +//- /p1/Cargo.toml +[package] +name = "p1" +version = "0.1.0" +edition = "2021" + "#, + r#" +//- /rust-analyzer.toml +hover.show.traitAssocItems = 4 + "#, + r#" +//- /p1/src/lib.rs +trait RandomTrait { + type B; + fn abc() -> i32; + fn def() -> i64; +} + +fn main() { + let a = RandomTrait; +}"#, + ], + vec![], + None, + ); + + server.query(QueryType::GlobalHover, 2); +} + +#[test] +fn ratoml_root_is_updateable() { + let mut server = RatomlTest::new( + vec![ + r#" +//- /p1/Cargo.toml +[package] +name = "p1" +version = "0.1.0" +edition = "2021" + "#, + r#" +//- /rust-analyzer.toml +hover.show.traitAssocItems = 4 + "#, + r#" +//- /p1/src/lib.rs +trait RandomTrait { + type B; + fn abc() -> i32; + fn def() -> i64; +} + +fn main() { + let a = RandomTrait; +}"#, + ], + vec![], + None, + ); + + assert!(server.query(QueryType::GlobalHover, 2)); + server.edit(1, RatomlTest::GLOBAL_TRAIT_ASSOC_ITEMS_ZERO.to_owned()); + assert!(!server.query(QueryType::GlobalHover, 2)); +} + +#[test] +fn ratoml_root_is_deletable() { + let mut server = RatomlTest::new( + vec![ + r#" +//- /p1/Cargo.toml +[package] +name = "p1" +version = "0.1.0" +edition = "2021" + "#, + r#" +//- /rust-analyzer.toml +hover.show.traitAssocItems = 4 + "#, + r#" +//- /p1/src/lib.rs +trait RandomTrait { + type B; + fn abc() -> i32; + fn def() -> i64; +} + +fn main() { + let a = RandomTrait; +}"#, + ], + vec![], + None, + ); + + assert!(server.query(QueryType::GlobalHover, 2)); + server.delete(1); + assert!(!server.query(QueryType::GlobalHover, 2)); +} diff --git a/crates/rust-analyzer/tests/slow-tests/support.rs b/crates/rust-analyzer/tests/slow-tests/support.rs index 17485ee3ae80..d12d0be53926 100644 --- a/crates/rust-analyzer/tests/slow-tests/support.rs +++ b/crates/rust-analyzer/tests/slow-tests/support.rs @@ -207,8 +207,8 @@ impl Project<'_> { change.change_client_config(self.config); let mut error_sink = ConfigError::default(); - assert!(error_sink.is_empty()); - config = config.apply_change(change, &mut error_sink); + assert!(error_sink.is_empty(), "{error_sink:?}"); + (config, _) = config.apply_change(change, &mut error_sink); config.rediscover_workspaces(); Server::new(tmp_dir.keep(), config)