diff --git a/Cargo.lock b/Cargo.lock index a6e460134f21..3c9c009b8de4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -323,6 +323,27 @@ dependencies = [ "syn", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "dissimilar" version = "1.0.7" @@ -898,6 +919,17 @@ dependencies = [ "libc", ] +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.2", + "libc", + "redox_syscall", +] + [[package]] name = "limit" version = "0.0.0" @@ -1186,6 +1218,12 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parking_lot" version = "0.12.1" @@ -1566,6 +1604,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "rowan" version = "0.15.15" @@ -1587,6 +1636,7 @@ dependencies = [ "anyhow", "cfg", "crossbeam-channel", + "dirs", "dissimilar", "expect-test", "flycheck", diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index 431aa30e56f2..e9408bf89766 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -273,10 +273,17 @@ impl Analysis { self.with_db(|db| status::status(db, file_id)) } - pub fn source_root(&self, file_id: FileId) -> Cancellable { + pub fn source_root_id(&self, file_id: FileId) -> Cancellable { self.with_db(|db| db.file_source_root(file_id)) } + pub fn is_local_source_root(&self, source_root_id: SourceRootId) -> Cancellable { + self.with_db(|db| { + let sr = db.source_root(source_root_id); + !sr.is_library + }) + } + pub fn parallel_prime_caches(&self, num_worker_threads: u8, cb: F) -> Cancellable<()> where F: Fn(ParallelPrimeCachesProgress) + Sync + std::panic::UnwindSafe, diff --git a/crates/paths/src/lib.rs b/crates/paths/src/lib.rs index 2d3653401d2f..f0a6bd4f7e30 100644 --- a/crates/paths/src/lib.rs +++ b/crates/paths/src/lib.rs @@ -135,6 +135,24 @@ impl AbsPathBuf { pub fn pop(&mut self) -> bool { self.0.pop() } + + /// Equivalent of [`PathBuf::push`] for `AbsPathBuf`. + /// + /// Extends `self` with `path`. + /// + /// If `path` is absolute, it replaces the current path. + /// + /// On Windows: + /// + /// * if `path` has a root but no prefix (e.g., `\windows`), it + /// replaces everything except for the prefix (if any) of `self`. + /// * if `path` has a prefix but no root, it replaces `self`. + /// * 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) { + self.0.push(suffix) + } } impl fmt::Display for AbsPathBuf { diff --git a/crates/rust-analyzer/Cargo.toml b/crates/rust-analyzer/Cargo.toml index 1cc3343e5fc4..235e4e15e0b5 100644 --- a/crates/rust-analyzer/Cargo.toml +++ b/crates/rust-analyzer/Cargo.toml @@ -21,6 +21,7 @@ path = "src/bin/main.rs" [dependencies] anyhow.workspace = true crossbeam-channel = "0.5.5" +dirs = "5.0.1" dissimilar.workspace = true itertools.workspace = true scip = "0.3.3" diff --git a/crates/rust-analyzer/src/bin/main.rs b/crates/rust-analyzer/src/bin/main.rs index 78920f3abace..c620fc3d0866 100644 --- a/crates/rust-analyzer/src/bin/main.rs +++ b/crates/rust-analyzer/src/bin/main.rs @@ -15,7 +15,11 @@ use std::{env, fs, path::PathBuf, process::ExitCode, sync::Arc}; use anyhow::Context; use lsp_server::Connection; -use rust_analyzer::{cli::flags, config::Config, from_json}; +use rust_analyzer::{ + cli::flags, + config::{Config, ConfigChange, ConfigError}, + from_json, +}; use semver::Version; use tracing_subscriber::fmt::writer::BoxMakeWriter; use vfs::AbsPathBuf; @@ -220,16 +224,20 @@ fn run_server() -> anyhow::Result<()> { .filter(|workspaces| !workspaces.is_empty()) .unwrap_or_else(|| vec![root_path.clone()]); let mut config = - Config::new(root_path, capabilities, workspace_roots, visual_studio_code_version); + Config::new(root_path, capabilities, workspace_roots, visual_studio_code_version, None); if let Some(json) = initialization_options { - if let Err(e) = config.update(json) { + let mut change = ConfigChange::default(); + change.change_client_config(json); + let mut error_sink = ConfigError::default(); + config = config.apply_change(change, &mut error_sink); + if !error_sink.is_empty() { use lsp_types::{ notification::{Notification, ShowMessage}, MessageType, ShowMessageParams, }; let not = lsp_server::Notification::new( ShowMessage::METHOD.to_owned(), - ShowMessageParams { typ: MessageType::WARNING, message: e.to_string() }, + ShowMessageParams { typ: MessageType::WARNING, message: error_sink.to_string() }, ); connection.sender.send(lsp_server::Message::Notification(not)).unwrap(); } diff --git a/crates/rust-analyzer/src/cli/scip.rs b/crates/rust-analyzer/src/cli/scip.rs index aef2c1be2249..b2d70562890f 100644 --- a/crates/rust-analyzer/src/cli/scip.rs +++ b/crates/rust-analyzer/src/cli/scip.rs @@ -10,9 +10,11 @@ use ide_db::LineIndexDatabase; use load_cargo::{load_workspace_at, LoadCargoConfig, ProcMacroServerChoice}; use rustc_hash::{FxHashMap, FxHashSet}; use scip::types as scip_types; +use tracing::error; use crate::{ cli::flags, + config::{ConfigChange, ConfigError}, line_index::{LineEndings, LineIndex, PositionEncoding}, }; @@ -35,12 +37,18 @@ impl flags::Scip { lsp_types::ClientCapabilities::default(), vec![], None, + None, ); if let Some(p) = self.config_path { let mut file = std::io::BufReader::new(std::fs::File::open(p)?); let json = serde_json::from_reader(&mut file)?; - config.update(json)?; + let mut change = ConfigChange::default(); + change.change_client_config(json); + let mut error_sink = ConfigError::default(); + config = config.apply_change(change, &mut error_sink); + // FIXME @alibektas : What happens to errors without logging? + error!(?error_sink, "Config Error(s)"); } let cargo_config = config.cargo(); let (db, vfs, _) = load_workspace_at( diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index 14a418565429..dbeb7f85a40a 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -1,14 +1,12 @@ //! Config used by the language server. //! -//! We currently get this config from `initialize` LSP request, which is not the -//! best way to do it, but was the simplest thing we could implement. -//! //! Of particular interest is the `feature_flags` hash map: while other fields //! configure the server itself, feature flags are passed into analysis, and //! tweak things like automatic insertion of `()` in completions. use std::{fmt, iter, ops::Not}; use cfg::{CfgAtom, CfgDiff}; +use dirs::config_dir; use flycheck::{CargoOptions, FlycheckConfig}; use ide::{ AssistConfig, CallableSnippets, CompletionConfig, DiagnosticsConfig, ExprFillDefaultMode, @@ -29,9 +27,14 @@ use project_model::{ }; use rustc_hash::{FxHashMap, FxHashSet}; use semver::Version; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde::{ + de::{DeserializeOwned, Error}, + ser::SerializeStruct, + Deserialize, Serialize, +}; use stdx::format_to_acc; -use vfs::{AbsPath, AbsPathBuf}; +use triomphe::Arc; +use vfs::{AbsPath, AbsPathBuf, FileId, VfsPath}; use crate::{ caps::completion_item_edit_resolve, @@ -67,12 +70,6 @@ config_data! { /// /// A config is searched for by traversing a "config tree" in a bottom up fashion. It is chosen by the nearest first principle. global: struct GlobalDefaultConfigData <- GlobalConfigInput -> { - /// Whether to insert #[must_use] when generating `as_` methods - /// for enum variants. - assist_emitMustUse: bool = false, - /// Placeholder expression to use for missing expressions in assists. - assist_expressionFillDefault: ExprFillDefaultDef = ExprFillDefaultDef::Todo, - /// Warm up caches on project load. cachePriming_enable: bool = true, /// How many worker threads to handle priming caches. The default `0` means to pick automatically. @@ -247,6 +244,69 @@ config_data! { /// If false, `-p ` will be passed instead. check_workspace: bool = true, + + /// Toggles the additional completions that automatically add imports when completed. + /// Note that your client must specify the `additionalTextEdits` LSP client capability to truly have this feature enabled. + completion_autoimport_enable: bool = true, + /// Toggles the additional completions that automatically show method calls and field accesses + /// with `self` prefixed to them when inside a method. + completion_autoself_enable: bool = true, + /// Whether to add parenthesis and argument snippets when completing function. + completion_callable_snippets: CallableCompletionDef = CallableCompletionDef::FillArguments, + /// Whether to show full function/method signatures in completion docs. + completion_fullFunctionSignatures_enable: bool = false, + /// Maximum number of completions to return. If `None`, the limit is infinite. + completion_limit: Option = None, + /// Whether to show postfix snippets like `dbg`, `if`, `not`, etc. + completion_postfix_enable: bool = true, + /// Enables completions of private items and fields that are defined in the current workspace even if they are not visible at the current position. + completion_privateEditable_enable: bool = false, + /// Custom completion snippets. + // NOTE: we use IndexMap for deterministic serialization ordering + completion_snippets_custom: IndexMap = serde_json::from_str(r#"{ + "Arc::new": { + "postfix": "arc", + "body": "Arc::new(${receiver})", + "requires": "std::sync::Arc", + "description": "Put the expression into an `Arc`", + "scope": "expr" + }, + "Rc::new": { + "postfix": "rc", + "body": "Rc::new(${receiver})", + "requires": "std::rc::Rc", + "description": "Put the expression into an `Rc`", + "scope": "expr" + }, + "Box::pin": { + "postfix": "pinbox", + "body": "Box::pin(${receiver})", + "requires": "std::boxed::Box", + "description": "Put the expression into a pinned `Box`", + "scope": "expr" + }, + "Ok": { + "postfix": "ok", + "body": "Ok(${receiver})", + "description": "Wrap the expression in a `Result::Ok`", + "scope": "expr" + }, + "Err": { + "postfix": "err", + "body": "Err(${receiver})", + "description": "Wrap the expression in a `Result::Err`", + "scope": "expr" + }, + "Some": { + "postfix": "some", + "body": "Some(${receiver})", + "description": "Wrap the expression in an `Option::Some`", + "scope": "expr" + } + }"#).unwrap(), + /// Whether to enable term search based snippets like `Some(foo.bar().baz())`. + completion_termSearch_enable: bool = false, + /// List of rust-analyzer diagnostics to disable. diagnostics_disabled: FxHashSet = FxHashSet::default(), /// Whether to show native rust-analyzer diagnostics. @@ -437,70 +497,13 @@ config_data! { } config_data! { - /// Local configurations can be overridden for every crate by placing a `rust-analyzer.toml` on crate root. - /// A config is searched for by traversing a "config tree" in a bottom up fashion. It is chosen by the nearest first principle. + /// Local configurations can be defined per `SourceRoot`. This almost always corresponds to a `Crate`. local: struct LocalDefaultConfigData <- LocalConfigInput -> { - /// Toggles the additional completions that automatically add imports when completed. - /// Note that your client must specify the `additionalTextEdits` LSP client capability to truly have this feature enabled. - completion_autoimport_enable: bool = true, - /// Toggles the additional completions that automatically show method calls and field accesses - /// with `self` prefixed to them when inside a method. - completion_autoself_enable: bool = true, - /// Whether to add parenthesis and argument snippets when completing function. - completion_callable_snippets: CallableCompletionDef = CallableCompletionDef::FillArguments, - /// Whether to show full function/method signatures in completion docs. - completion_fullFunctionSignatures_enable: bool = false, - /// Maximum number of completions to return. If `None`, the limit is infinite. - completion_limit: Option = None, - /// Whether to show postfix snippets like `dbg`, `if`, `not`, etc. - completion_postfix_enable: bool = true, - /// Enables completions of private items and fields that are defined in the current workspace even if they are not visible at the current position. - completion_privateEditable_enable: bool = false, - /// Custom completion snippets. - // NOTE: we use IndexMap for deterministic serialization ordering - completion_snippets_custom: IndexMap = serde_json::from_str(r#"{ - "Arc::new": { - "postfix": "arc", - "body": "Arc::new(${receiver})", - "requires": "std::sync::Arc", - "description": "Put the expression into an `Arc`", - "scope": "expr" - }, - "Rc::new": { - "postfix": "rc", - "body": "Rc::new(${receiver})", - "requires": "std::rc::Rc", - "description": "Put the expression into an `Rc`", - "scope": "expr" - }, - "Box::pin": { - "postfix": "pinbox", - "body": "Box::pin(${receiver})", - "requires": "std::boxed::Box", - "description": "Put the expression into a pinned `Box`", - "scope": "expr" - }, - "Ok": { - "postfix": "ok", - "body": "Ok(${receiver})", - "description": "Wrap the expression in a `Result::Ok`", - "scope": "expr" - }, - "Err": { - "postfix": "err", - "body": "Err(${receiver})", - "description": "Wrap the expression in a `Result::Err`", - "scope": "expr" - }, - "Some": { - "postfix": "some", - "body": "Some(${receiver})", - "description": "Wrap the expression in an `Option::Some`", - "scope": "expr" - } - }"#).unwrap(), - /// Whether to enable term search based snippets like `Some(foo.bar().baz())`. - completion_termSearch_enable: bool = false, + /// Whether to insert #[must_use] when generating `as_` methods + /// for enum variants. + assist_emitMustUse: bool = false, + /// Placeholder expression to use for missing expressions in assists. + assist_expressionFillDefault: ExprFillDefaultDef = ExprFillDefaultDef::Todo, /// Enables highlighting of related references while the cursor is on `break`, `loop`, `while`, or `for` keywords. highlightRelated_breakPoints_enable: bool = true, @@ -640,23 +643,304 @@ pub struct Config { workspace_roots: Vec, caps: lsp_types::ClientCapabilities, root_path: AbsPathBuf, - detached_files: Vec, snippets: Vec, visual_studio_code_version: Option, default_config: DefaultConfigData, - client_config: FullConfigInput, - user_config: GlobalLocalConfigInput, - #[allow(dead_code)] + /// Config node that obtains its initial value during the server initialization and + /// by receiving a `lsp_types::notification::DidChangeConfiguration`. + client_config: ClientConfig, + + /// 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 : + /// + /// |Platform | Value | Example | + /// | ------- | ------------------------------------- | ---------------------------------------- | + /// | Linux | `$XDG_CONFIG_HOME` or `$HOME`/.config | /home/alice/.config | + /// | macOS | `$HOME`/Library/Application Support | /Users/Alice/Library/Application Support | + /// | Windows | `{FOLDERID_RoamingAppData}` | C:\Users\Alice\AppData\Roaming | + user_config_path: VfsPath, + + /// FIXME @alibektas : Change this to sth better. + /// Config node whose values apply to **every** Rust project. + 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, + + /// For every `SourceRoot` there can be at most one RATOML file. 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 { - #[allow(dead_code)] 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)] - parent: Option, + /// 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 { + 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; + } + + if let Some(mut json) = change.client_config_change { + tracing::info!("updating config from JSON: {:#}", json); + if !(json.is_null() || json.as_object().map_or(false, |it| it.is_empty())) { + let detached_files = get_field::>( + &mut json, + &mut json_errors, + "detachedFiles", + None, + ) + .unwrap_or_default() + .into_iter() + .map(AbsPathBuf::assert) + .collect(); + + 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.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.ratoml_file_change { + for (source_root_id, (file_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 Some(source_root_map) = change.source_map_change { + config.source_root_parent_map = source_root_map; + } + + let snips = self.completion_snippets_custom().to_owned(); + + for (name, def) in snips.iter() { + if def.prefix.is_empty() && def.postfix.is_empty() { + continue; + } + let scope = match def.scope { + SnippetScopeDef::Expr => SnippetScope::Expr, + SnippetScopeDef::Type => SnippetScope::Type, + SnippetScopeDef::Item => SnippetScope::Item, + }; + match Snippet::new( + &def.prefix, + &def.postfix, + &def.body, + def.description.as_ref().unwrap_or(name), + &def.requires, + scope, + ) { + Some(snippet) => config.snippets.push(snippet), + None => error_sink.0.push(ConfigErrorInner::JsonError( + format!("snippet {name} is invalid"), + ::custom( + "snippet path is invalid or triggers are missing", + ), + )), + } + } + + if config.check_command().is_empty() { + error_sink.0.push(ConfigErrorInner::JsonError( + "/check/command".to_owned(), + serde_json::Error::custom("expected a non-empty string"), + )); + } + config + } +} + +#[derive(Default, Debug)] +pub struct ConfigChange { + user_config_change: Option<(FileId, String)>, + root_ratoml_change: Option<(FileId, String)>, + client_config_change: Option, + ratoml_file_change: Option)>>, + source_map_change: Option>>, +} + +impl ConfigChange { + pub fn change_ratoml( + &mut self, + source_root: SourceRootId, + file_id: FileId, + vfs_path: VfsPath, + content: Option, + ) -> Option<(FileId, VfsPath, Option)> { + if let Some(changes) = self.ratoml_file_change.as_mut() { + changes.insert(source_root, (file_id, vfs_path, content)) + } else { + let mut map = FxHashMap::default(); + map.insert(source_root, (file_id, vfs_path, content)); + self.ratoml_file_change = Some(map); + None + } + } + + pub fn change_user_config(&mut self, content: Option<(FileId, String)>) { + 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)>) { + assert!(self.user_config_change.is_none()); // Otherwise it is a double write. + self.root_ratoml_change = content; + } + + pub fn change_client_config(&mut self, change: serde_json::Value) { + self.client_config_change = Some(change); + } + + pub fn change_source_root_parent_map( + &mut self, + source_root_map: Arc>, + ) { + assert!(self.source_map_change.is_none()); + self.source_map_change = Some(source_root_map.clone()); + } } macro_rules! try_ { @@ -845,23 +1129,37 @@ pub struct ClientCommandsConfig { } #[derive(Debug)] -pub struct ConfigError { - errors: Vec<(String, serde_json::Error)>, +pub enum ConfigErrorInner { + JsonError(String, serde_json::Error), + Toml(String, toml::de::Error), +} + +#[derive(Debug, Default)] +pub struct ConfigError(Vec); + +impl ConfigError { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } } +impl ConfigError {} + impl fmt::Display for ConfigError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let errors = self.errors.iter().format_with("\n", |(key, e), f| { - f(key)?; - f(&": ")?; - f(e) + let errors = self.0.iter().format_with("\n", |inner, f| match inner { + ConfigErrorInner::JsonError(key, e) => { + f(key)?; + f(&": ")?; + f(e) + } + ConfigErrorInner::Toml(key, e) => { + f(key)?; + f(&": ")?; + f(e) + } }); - write!( - f, - "invalid config value{}:\n{}", - if self.errors.len() == 1 { "" } else { "s" }, - errors - ) + write!(f, "invalid config value{}:\n{}", if self.0.len() == 1 { "" } else { "s" }, errors) } } @@ -873,19 +1171,45 @@ impl Config { caps: ClientCapabilities, workspace_roots: Vec, visual_studio_code_version: Option, + user_config_path: Option, ) -> Self { + let user_config_path = if let Some(user_config_path) = user_config_path { + user_config_path.join("rust-analyzer").join("rust-analyzer.toml") + } else { + let p = config_dir() + .expect("A config dir is expected to existed on all platforms ra supports.") + .join("rust-analyzer") + .join("rust-analyzer.toml"); + Utf8PathBuf::from_path_buf(p).expect("Config dir expected to be abs.") + }; + + // A user config cannot be a virtual path as rust-analyzer cannot support watching changes in virtual paths. + // See `GlobalState::process_changes` to get more info. + // FIXME @alibektas : Temporary solution. I don't think this is right as at some point we may allow users to specify + // custom USER_CONFIG_PATHs which may also be relative. + let user_config_path = VfsPath::from(AbsPathBuf::assert(user_config_path)); + let root_ratoml_path = { + let mut p = root_path.clone(); + p.push("rust-analyzer.toml"); + VfsPath::new_real_path(p.to_string()) + }; + Config { caps, - detached_files: Vec::new(), discovered_projects: Vec::new(), root_path, snippets: Default::default(), workspace_roots, visual_studio_code_version, - client_config: FullConfigInput::default(), - user_config: GlobalLocalConfigInput::default(), + client_config: ClientConfig::default(), + user_config: None, ratoml_files: FxHashMap::default(), default_config: DefaultConfigData::default(), + source_root_parent_map: Arc::new(FxHashMap::default()), + user_config_path, + root_ratoml: None, + root_ratoml_path, + should_update: false, } } @@ -908,71 +1232,6 @@ impl Config { self.workspace_roots.extend(paths); } - pub fn update(&mut self, mut json: serde_json::Value) -> Result<(), ConfigError> { - tracing::info!("updating config from JSON: {:#}", json); - if json.is_null() || json.as_object().map_or(false, |it| it.is_empty()) { - return Ok(()); - } - let mut errors = Vec::new(); - self.detached_files = - get_field::>(&mut json, &mut errors, "detachedFiles", None) - .unwrap_or_default() - .into_iter() - .map(AbsPathBuf::assert) - .collect(); - patch_old_style::patch_json_for_outdated_configs(&mut json); - self.client_config = FullConfigInput::from_json(json, &mut errors); - tracing::debug!(?self.client_config, "deserialized config data"); - self.snippets.clear(); - - let snips = self.completion_snippets_custom(None).to_owned(); - - for (name, def) in snips.iter() { - if def.prefix.is_empty() && def.postfix.is_empty() { - continue; - } - let scope = match def.scope { - SnippetScopeDef::Expr => SnippetScope::Expr, - SnippetScopeDef::Type => SnippetScope::Type, - SnippetScopeDef::Item => SnippetScope::Item, - }; - match Snippet::new( - &def.prefix, - &def.postfix, - &def.body, - def.description.as_ref().unwrap_or(name), - &def.requires, - scope, - ) { - Some(snippet) => self.snippets.push(snippet), - None => errors.push(( - format!("snippet {name} is invalid"), - ::custom( - "snippet path is invalid or triggers are missing", - ), - )), - } - } - - self.validate(&mut errors); - - if errors.is_empty() { - Ok(()) - } else { - Err(ConfigError { errors }) - } - } - - fn validate(&self, error_sink: &mut Vec<(String, serde_json::Error)>) { - use serde::de::Error; - if self.check_command().is_empty() { - error_sink.push(( - "/check/command".to_owned(), - serde_json::Error::custom("expected a non-empty string"), - )); - } - } - pub fn json_schema() -> serde_json::Value { FullConfigInput::json_schema() } @@ -981,12 +1240,12 @@ impl Config { &self.root_path } - pub fn caps(&self) -> &lsp_types::ClientCapabilities { - &self.caps + pub fn root_ratoml_path(&self) -> &VfsPath { + &self.root_ratoml_path } - pub fn detached_files(&self) -> &[AbsPathBuf] { - &self.detached_files + pub fn caps(&self) -> &lsp_types::ClientCapabilities { + &self.caps } } @@ -997,22 +1256,20 @@ impl Config { allowed: None, insert_use: self.insert_use_config(source_root), prefer_no_std: self.imports_preferNoStd(source_root).to_owned(), - assist_emit_must_use: self.assist_emitMustUse().to_owned(), + assist_emit_must_use: self.assist_emitMustUse(source_root).to_owned(), prefer_prelude: self.imports_preferPrelude(source_root).to_owned(), } } pub fn completion(&self, source_root: Option) -> CompletionConfig { CompletionConfig { - enable_postfix_completions: self.completion_postfix_enable(source_root).to_owned(), - enable_imports_on_the_fly: self.completion_autoimport_enable(source_root).to_owned() + enable_postfix_completions: self.completion_postfix_enable().to_owned(), + enable_imports_on_the_fly: self.completion_autoimport_enable().to_owned() && completion_item_edit_resolve(&self.caps), - enable_self_on_the_fly: self.completion_autoself_enable(source_root).to_owned(), - enable_private_editable: self.completion_privateEditable_enable(source_root).to_owned(), - full_function_signatures: self - .completion_fullFunctionSignatures_enable(source_root) - .to_owned(), - callable: match self.completion_callable_snippets(source_root) { + enable_self_on_the_fly: self.completion_autoself_enable().to_owned(), + enable_private_editable: self.completion_privateEditable_enable().to_owned(), + full_function_signatures: self.completion_fullFunctionSignatures_enable().to_owned(), + callable: match self.completion_callable_snippets() { CallableCompletionDef::FillArguments => Some(CallableSnippets::FillArguments), CallableCompletionDef::AddParentheses => Some(CallableSnippets::AddParentheses), CallableCompletionDef::None => None, @@ -1030,12 +1287,18 @@ impl Config { .snippet_support? )), snippets: self.snippets.clone().to_vec(), - limit: self.completion_limit(source_root).to_owned(), - enable_term_search: self.completion_termSearch_enable(source_root).to_owned(), + limit: self.completion_limit().to_owned(), + enable_term_search: self.completion_termSearch_enable().to_owned(), prefer_prelude: self.imports_preferPrelude(source_root).to_owned(), } } + 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 + } + pub fn diagnostics(&self, source_root: Option) -> DiagnosticsConfig { DiagnosticsConfig { enabled: *self.diagnostics_enable(), @@ -1043,7 +1306,7 @@ impl Config { proc_macros_enabled: *self.procMacro_enable(), disable_experimental: !self.diagnostics_experimental_enable(), disabled: self.diagnostics_disabled().clone(), - expr_fill_default: match self.assist_expressionFillDefault() { + expr_fill_default: match self.assist_expressionFillDefault(source_root) { ExprFillDefaultDef::Todo => ExprFillDefaultMode::Todo, ExprFillDefaultDef::Default => ExprFillDefaultMode::Default, }, @@ -2005,7 +2268,7 @@ enum SnippetScopeDef { #[derive(Serialize, Deserialize, Debug, Clone, Default)] #[serde(default)] -struct SnippetDef { +pub(crate) struct SnippetDef { #[serde(with = "single_or_array")] #[serde(skip_serializing_if = "Vec::is_empty")] prefix: Vec, @@ -2100,7 +2363,7 @@ enum ImportGranularityDef { #[derive(Serialize, Deserialize, Debug, Copy, Clone)] #[serde(rename_all = "snake_case")] -enum CallableCompletionDef { +pub(crate) enum CallableCompletionDef { FillArguments, AddParentheses, None, @@ -2307,15 +2570,30 @@ macro_rules! _impl_for_config_data { $( $($doc)* #[allow(non_snake_case)] - $vis fn $field(&self, _source_root: Option) -> &$ty { - if let Some(v) = self.client_config.local.$field.as_ref() { - return &v; + $vis fn $field(&self, source_root: Option) -> &$ty { + + if source_root.is_some() { + let mut par: Option = source_root; + 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() { + return value; + } + } + } } - if let Some(v) = self.user_config.local.$field.as_ref() { + if let Some(v) = self.client_config.node.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() { + return &v; + } + } + &self.default_config.local.$field } )* @@ -2331,14 +2609,23 @@ macro_rules! _impl_for_config_data { $($doc)* #[allow(non_snake_case)] $vis fn $field(&self) -> &$ty { - if let Some(v) = self.client_config.global.$field.as_ref() { - return &v; + + if let Some(root_path_ratoml) = self.root_ratoml.as_ref() { + if let Some(v) = root_path_ratoml.node.global.$field.as_ref() { + return &v; + } } - if let Some(v) = self.user_config.global.$field.as_ref() { + if let Some(v) = self.client_config.node.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() { + return &v; + } + } + &self.default_config.global.$field } )* @@ -2491,11 +2778,10 @@ struct DefaultConfigData { /// All of the config levels, all fields `Option`, to describe fields that are actually set by /// some rust-analyzer.toml file or JSON blob. An empty rust-analyzer.toml corresponds to /// all fields being None. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, Serialize)] struct FullConfigInput { global: GlobalConfigInput, local: LocalConfigInput, - #[allow(dead_code)] client: ClientConfigInput, } @@ -2534,7 +2820,7 @@ impl FullConfigInput { /// All of the config levels, all fields `Option`, to describe fields that are actually set by /// some rust-analyzer.toml file or JSON blob. An empty rust-analyzer.toml corresponds to /// all fields being None. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, Serialize)] struct GlobalLocalConfigInput { global: GlobalConfigInput, local: LocalConfigInput, @@ -3090,12 +3376,17 @@ mod tests { Default::default(), vec![], None, + None, ); - config - .update(serde_json::json!({ - "procMacro_server": null, - })) - .unwrap(); + + let mut change = ConfigChange::default(); + change.change_client_config(serde_json::json!({ + "procMacro" : { + "server": null, + }})); + + let mut error_sink = ConfigError::default(); + config = config.apply_change(change, &mut error_sink); assert_eq!(config.proc_macro_srv(), None); } @@ -3106,12 +3397,16 @@ mod tests { Default::default(), vec![], None, + None, ); - config - .update(serde_json::json!({ - "procMacro": {"server": project_root().display().to_string()} - })) - .unwrap(); + let mut change = ConfigChange::default(); + change.change_client_config(serde_json::json!({ + "procMacro" : { + "server": project_root().display().to_string(), + }})); + + let mut error_sink = ConfigError::default(); + config = config.apply_change(change, &mut error_sink); assert_eq!(config.proc_macro_srv(), Some(AbsPathBuf::try_from(project_root()).unwrap())); } @@ -3122,12 +3417,19 @@ mod tests { Default::default(), vec![], None, + None, ); - config - .update(serde_json::json!({ - "procMacro": {"server": "./server"} - })) - .unwrap(); + + let mut change = ConfigChange::default(); + + change.change_client_config(serde_json::json!({ + "procMacro" : { + "server": "./server" + }})); + + let mut error_sink = ConfigError::default(); + config = config.apply_change(change, &mut error_sink); + assert_eq!( config.proc_macro_srv(), Some(AbsPathBuf::try_from(project_root().join("./server")).unwrap()) @@ -3141,12 +3443,17 @@ mod tests { Default::default(), vec![], None, + None, ); - config - .update(serde_json::json!({ - "rust": { "analyzerTargetDir": null } - })) - .unwrap(); + + let mut change = ConfigChange::default(); + + change.change_client_config(serde_json::json!({ + "rust" : { "analyzerTargetDir" : null } + })); + + let mut error_sink = ConfigError::default(); + 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()) @@ -3160,12 +3467,17 @@ mod tests { Default::default(), vec![], None, + None, ); - config - .update(serde_json::json!({ - "rust": { "analyzerTargetDir": true } - })) - .unwrap(); + + let mut change = ConfigChange::default(); + change.change_client_config(serde_json::json!({ + "rust" : { "analyzerTargetDir" : true } + })); + + let mut error_sink = ConfigError::default(); + config = config.apply_change(change, &mut error_sink); + assert_eq!(config.cargo_targetDir(), &Some(TargetDirectory::UseSubdirectory(true))); assert!( matches!(config.flycheck(), FlycheckConfig::CargoCommand { options, .. } if options.target_dir == Some(Utf8PathBuf::from("target/rust-analyzer"))) @@ -3179,12 +3491,17 @@ mod tests { Default::default(), vec![], None, + None, ); - config - .update(serde_json::json!({ - "rust": { "analyzerTargetDir": "other_folder" } - })) - .unwrap(); + + let mut change = ConfigChange::default(); + change.change_client_config(serde_json::json!({ + "rust" : { "analyzerTargetDir" : "other_folder" } + })); + + let mut error_sink = ConfigError::default(); + config = config.apply_change(change, &mut error_sink); + assert_eq!( config.cargo_targetDir(), &Some(TargetDirectory::Directory(Utf8PathBuf::from("other_folder"))) diff --git a/crates/rust-analyzer/src/diagnostics.rs b/crates/rust-analyzer/src/diagnostics.rs index 65a9a4914934..6dc608c77762 100644 --- a/crates/rust-analyzer/src/diagnostics.rs +++ b/crates/rust-analyzer/src/diagnostics.rs @@ -154,7 +154,7 @@ pub(crate) fn fetch_native_diagnostics( .copied() .filter_map(|file_id| { let line_index = snapshot.file_line_index(file_id).ok()?; - let source_root = snapshot.analysis.source_root(file_id).ok()?; + let source_root = snapshot.analysis.source_root_id(file_id).ok()?; let diagnostics = snapshot .analysis diff --git a/crates/rust-analyzer/src/diagnostics/to_proto.rs b/crates/rust-analyzer/src/diagnostics/to_proto.rs index 3d3f94401991..4832e8cab43f 100644 --- a/crates/rust-analyzer/src/diagnostics/to_proto.rs +++ b/crates/rust-analyzer/src/diagnostics/to_proto.rs @@ -547,6 +547,7 @@ mod tests { ClientCapabilities::default(), Vec::new(), None, + None, ), ); let snap = state.snapshot(); diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs index ec10bc7ccdaa..adca0373d883 100644 --- a/crates/rust-analyzer/src/global_state.rs +++ b/crates/rust-analyzer/src/global_state.rs @@ -20,14 +20,18 @@ use parking_lot::{ use proc_macro_api::ProcMacroServer; use project_model::{CargoWorkspace, ProjectWorkspace, Target, WorkspaceBuildScripts}; use rustc_hash::{FxHashMap, FxHashSet}; +use tracing::{error, warn}; use triomphe::Arc; -use vfs::{AnchoredPathBuf, ChangedFile, Vfs}; +use vfs::{AnchoredPathBuf, ChangedFile, Vfs, VfsPath}; use crate::{ - config::{Config, ConfigError}, + config::{Config, ConfigChange, ConfigError}, diagnostics::{CheckFixes, DiagnosticCollection}, line_index::{LineEndings, LineIndex}, - lsp::{from_proto, to_proto::url_from_abs_path}, + lsp::{ + from_proto::{self}, + to_proto::url_from_abs_path, + }, lsp_ext, main_loop::Task, mem_docs::MemDocs, @@ -67,7 +71,7 @@ pub(crate) struct GlobalState { pub(crate) mem_docs: MemDocs, pub(crate) source_root_config: SourceRootConfig, /// A mapping that maps a local source root's `SourceRootId` to it parent's `SourceRootId`, if it has one. - pub(crate) local_roots_parent_map: FxHashMap, + pub(crate) local_roots_parent_map: Arc>, pub(crate) semantic_tokens_cache: Arc>>, // status @@ -208,7 +212,7 @@ impl GlobalState { shutdown_requested: false, last_reported_status: None, source_root_config: SourceRootConfig::default(), - local_roots_parent_map: FxHashMap::default(), + local_roots_parent_map: Arc::new(FxHashMap::default()), config_errors: Default::default(), proc_macro_clients: Arc::from_iter([]), @@ -250,6 +254,24 @@ impl GlobalState { let _p = tracing::span!(tracing::Level::INFO, "GlobalState::process_changes").entered(); let mut file_changes = FxHashMap::<_, (bool, ChangedFile)>::default(); + + // We cannot directly resolve a change in a ratoml file to a format + // that can be used by the config module because config talks + // in `SourceRootId`s instead of `FileId`s and `FileId` -> `SourceRootId` + // mapping is not ready until `AnalysisHost::apply_changes` has been called. + let mut modified_ratoml_files: FxHashMap = FxHashMap::default(); + 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 root_vfs_path = { + let mut root_vfs_path = self.config.root_path().to_path_buf(); + root_vfs_path.push("rust-analyzer.toml"); + VfsPath::new_real_path(root_vfs_path.to_string()) + }; + let (change, modified_rust_files, workspace_structure_change) = { let mut change = ChangeWithProcMacros::new(); let mut guard = self.vfs.write(); @@ -303,14 +325,19 @@ impl GlobalState { }) .map(|(file_id, (_, change))| vfs::ChangedFile { file_id, ..change }) .collect(); - let mut workspace_structure_change = None; // A file was added or deleted let mut has_structure_changes = false; let mut bytes = vec![]; let mut modified_rust_files = vec![]; + for file in changed_files { let vfs_path = vfs.file_path(file.file_id); + if let Some(("rust-analyzer", Some("toml"))) = vfs_path.name_and_extension() { + // Remember ids to use them after `apply_changes` + modified_ratoml_files.insert(file.file_id, vfs_path.clone()); + } + if let Some(path) = vfs_path.as_path() { has_structure_changes = file.is_created_or_deleted(); @@ -348,10 +375,30 @@ impl GlobalState { } 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), + 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)); + } + } + } Some((text, line_endings)) => { line_endings_map.insert(file_id, line_endings); - change.change_file(file_id, Some(text)); + 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()))); + } + } } }); if has_structure_changes { @@ -363,6 +410,54 @@ impl GlobalState { 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) + { + // SourceRoot has more than 1 RATOML files. In this case lexicographically smaller wins. + if old_path < vfs_path { + warn!(?old_path, ?vfs_path, "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); + } + } + } + } else { + // Mapping to a SourceRoot should always end up in `Ok` + 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))); + } + + change + }; + + let mut error_sink = ConfigError::default(); + let config = 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 !matches!(&workspace_structure_change, Some((.., true))) { _ = self diff --git a/crates/rust-analyzer/src/handlers/notification.rs b/crates/rust-analyzer/src/handlers/notification.rs index 68dc2cf2e29f..fbe77cab88b2 100644 --- a/crates/rust-analyzer/src/handlers/notification.rs +++ b/crates/rust-analyzer/src/handlers/notification.rs @@ -13,7 +13,7 @@ use triomphe::Arc; use vfs::{AbsPathBuf, ChangeKind, VfsPath}; use crate::{ - config::Config, + config::{Config, ConfigChange, ConfigError}, global_state::GlobalState, lsp::{from_proto, utils::apply_document_changes}, lsp_ext::{self, RunFlycheckParams}, @@ -71,6 +71,7 @@ pub(crate) fn handle_did_open_text_document( tracing::error!("duplicate DidOpenTextDocument: {}", path); } + tracing::info!("New file content set {:?}", params.text_document.text); state.vfs.write().0.set_file_contents(path, Some(params.text_document.text.into_bytes())); if state.config.notifications().unindexed_project { tracing::debug!("queuing task"); @@ -192,10 +193,11 @@ pub(crate) fn handle_did_change_configuration( } (None, Some(mut configs)) => { if let Some(json) = configs.get_mut(0) { - // Note that json can be null according to the spec if the client can't - // provide a configuration. This is handled in Config::update below. let mut config = Config::clone(&*this.config); - this.config_errors = config.update(json.take()).err(); + 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); this.update_configuration(config); } } diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs index 9c86704ba3e4..2c5dbd145e8a 100644 --- a/crates/rust-analyzer/src/handlers/request.rs +++ b/crates/rust-analyzer/src/handlers/request.rs @@ -370,7 +370,7 @@ pub(crate) fn handle_join_lines( let _p = tracing::span!(tracing::Level::INFO, "handle_join_lines").entered(); let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; - let source_root = snap.analysis.source_root(file_id)?; + let source_root = snap.analysis.source_root_id(file_id)?; let config = snap.config.join_lines(Some(source_root)); let line_index = snap.file_line_index(file_id)?; @@ -938,7 +938,7 @@ pub(crate) fn handle_completion( let completion_trigger_character = params.context.and_then(|ctx| ctx.trigger_character).and_then(|s| s.chars().next()); - let source_root = snap.analysis.source_root(position.file_id)?; + let source_root = snap.analysis.source_root_id(position.file_id)?; let completion_config = &snap.config.completion(Some(source_root)); let items = match snap.analysis.completions( completion_config, @@ -980,7 +980,7 @@ pub(crate) fn handle_completion_resolve( let file_id = from_proto::file_id(&snap, &resolve_data.position.text_document.uri)?; let line_index = snap.file_line_index(file_id)?; let offset = from_proto::offset(&line_index, resolve_data.position.position)?; - let source_root = snap.analysis.source_root(file_id)?; + let source_root = snap.analysis.source_root_id(file_id)?; let additional_edits = snap .analysis @@ -1212,7 +1212,7 @@ pub(crate) fn handle_code_action( let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; let line_index = snap.file_line_index(file_id)?; let frange = from_proto::file_range(&snap, ¶ms.text_document, params.range)?; - let source_root = snap.analysis.source_root(file_id)?; + let source_root = snap.analysis.source_root_id(file_id)?; let mut assists_config = snap.config.assist(Some(source_root)); assists_config.allowed = params @@ -1285,7 +1285,7 @@ pub(crate) fn handle_code_action_resolve( let line_index = snap.file_line_index(file_id)?; let range = from_proto::text_range(&line_index, params.code_action_params.range)?; let frange = FileRange { file_id, range }; - let source_root = snap.analysis.source_root(file_id)?; + let source_root = snap.analysis.source_root_id(file_id)?; let mut assists_config = snap.config.assist(Some(source_root)); assists_config.allowed = params @@ -1439,7 +1439,7 @@ pub(crate) fn handle_document_highlight( let _p = tracing::span!(tracing::Level::INFO, "handle_document_highlight").entered(); let position = from_proto::file_position(&snap, params.text_document_position_params)?; let line_index = snap.file_line_index(position.file_id)?; - let source_root = snap.analysis.source_root(position.file_id)?; + let source_root = snap.analysis.source_root_id(position.file_id)?; let refs = match snap .analysis @@ -1490,7 +1490,7 @@ pub(crate) fn handle_inlay_hints( params.range, )?; let line_index = snap.file_line_index(file_id)?; - let source_root = snap.analysis.source_root(file_id)?; + let source_root = snap.analysis.source_root_id(file_id)?; let inlay_hints_config = snap.config.inlay_hints(Some(source_root)); Ok(Some( @@ -1524,7 +1524,7 @@ pub(crate) fn handle_inlay_hints_resolve( let line_index = snap.file_line_index(file_id)?; let hint_position = from_proto::offset(&line_index, original_hint.position)?; - let source_root = snap.analysis.source_root(file_id)?; + let source_root = snap.analysis.source_root_id(file_id)?; let mut forced_resolve_inlay_hints_config = snap.config.inlay_hints(Some(source_root)); forced_resolve_inlay_hints_config.fields_to_resolve = InlayFieldsToResolve::empty(); @@ -1658,7 +1658,7 @@ pub(crate) fn handle_semantic_tokens_full( let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; let text = snap.analysis.file_text(file_id)?; let line_index = snap.file_line_index(file_id)?; - let source_root = snap.analysis.source_root(file_id)?; + let source_root = snap.analysis.source_root_id(file_id)?; let mut highlight_config = snap.config.highlighting_config(Some(source_root)); // Avoid flashing a bunch of unresolved references when the proc-macro servers haven't been spawned yet. @@ -1689,7 +1689,7 @@ pub(crate) fn handle_semantic_tokens_full_delta( let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; let text = snap.analysis.file_text(file_id)?; let line_index = snap.file_line_index(file_id)?; - let source_root = snap.analysis.source_root(file_id)?; + let source_root = snap.analysis.source_root_id(file_id)?; let mut highlight_config = snap.config.highlighting_config(Some(source_root)); // Avoid flashing a bunch of unresolved references when the proc-macro servers haven't been spawned yet. @@ -1733,7 +1733,7 @@ pub(crate) fn handle_semantic_tokens_range( let frange = from_proto::file_range(&snap, ¶ms.text_document, params.range)?; let text = snap.analysis.file_text(frange.file_id)?; let line_index = snap.file_line_index(frange.file_id)?; - let source_root = snap.analysis.source_root(frange.file_id)?; + let source_root = snap.analysis.source_root_id(frange.file_id)?; let mut highlight_config = snap.config.highlighting_config(Some(source_root)); // Avoid flashing a bunch of unresolved references when the proc-macro servers haven't been spawned yet. @@ -1959,8 +1959,8 @@ fn goto_type_action_links( snap: &GlobalStateSnapshot, nav_targets: &[HoverGotoTypeData], ) -> Option { - if nav_targets.is_empty() - || !snap.config.hover_actions().goto_type_def + if !snap.config.hover_actions().goto_type_def + || nav_targets.is_empty() || !snap.config.client_commands().goto_location { return None; diff --git a/crates/rust-analyzer/src/lib.rs b/crates/rust-analyzer/src/lib.rs index 175ffa622ff7..d9b31550c562 100644 --- a/crates/rust-analyzer/src/lib.rs +++ b/crates/rust-analyzer/src/lib.rs @@ -18,7 +18,6 @@ mod cargo_target_spec; mod diagnostics; mod diff; mod dispatch; -mod global_state; mod hack_recover_crate_name; mod line_index; mod main_loop; @@ -40,6 +39,7 @@ pub mod tracing { } pub mod config; +pub mod global_state; pub mod lsp; use self::lsp::ext as lsp_ext; diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index f37b25fb9558..a0fad6f6acec 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -185,6 +185,11 @@ impl GlobalState { scheme: None, pattern: Some("**/Cargo.lock".into()), }, + lsp_types::DocumentFilter { + language: None, + scheme: None, + pattern: Some("**/rust-analyzer.toml".into()), + }, ]), }, }; diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs index d2e495dbfcd1..3fffda8dbe5c 100644 --- a/crates/rust-analyzer/src/reload.rs +++ b/crates/rust-analyzer/src/reload.rs @@ -24,14 +24,16 @@ use ide_db::{ }; use itertools::Itertools; use load_cargo::{load_proc_macro, ProjectFolders}; +use lsp_types::FileSystemWatcher; use proc_macro_api::ProcMacroServer; use project_model::{ProjectWorkspace, WorkspaceBuildScripts}; use stdx::{format_to, thread::ThreadIntent}; +use tracing::error; use triomphe::Arc; use vfs::{AbsPath, AbsPathBuf, ChangeKind}; use crate::{ - config::{Config, FilesWatcher, LinkedProject}, + config::{Config, ConfigChange, ConfigError, FilesWatcher, LinkedProject}, global_state::GlobalState, lsp_ext, main_loop::Task, @@ -431,40 +433,61 @@ impl GlobalState { let filter = self.workspaces.iter().flat_map(|ws| ws.to_roots()).filter(|it| it.is_local); - let watchers = if self.config.did_change_watched_files_relative_pattern_support() { - // When relative patterns are supported by the client, prefer using them - filter - .flat_map(|root| { - root.include.into_iter().flat_map(|base| { - [(base.clone(), "**/*.rs"), (base, "**/Cargo.{lock,toml}")] + let mut watchers: Vec = + if self.config.did_change_watched_files_relative_pattern_support() { + // When relative patterns are supported by the client, prefer using them + filter + .flat_map(|root| { + root.include.into_iter().flat_map(|base| { + [ + (base.clone(), "**/*.rs"), + (base.clone(), "**/Cargo.{lock,toml}"), + (base, "**/rust-analyzer.toml"), + ] + }) }) - }) - .map(|(base, pat)| lsp_types::FileSystemWatcher { - glob_pattern: lsp_types::GlobPattern::Relative( - lsp_types::RelativePattern { - base_uri: lsp_types::OneOf::Right( - lsp_types::Url::from_file_path(base).unwrap(), - ), - pattern: pat.to_owned(), - }, - ), - kind: None, - }) - .collect() - } else { - // When they're not, integrate the base to make them into absolute patterns - filter - .flat_map(|root| { - root.include.into_iter().flat_map(|base| { - [format!("{base}/**/*.rs"), format!("{base}/**/Cargo.{{lock,toml}}")] + .map(|(base, pat)| lsp_types::FileSystemWatcher { + glob_pattern: lsp_types::GlobPattern::Relative( + lsp_types::RelativePattern { + base_uri: lsp_types::OneOf::Right( + lsp_types::Url::from_file_path(base).unwrap(), + ), + pattern: pat.to_owned(), + }, + ), + kind: None, }) - }) + .collect() + } else { + // When they're not, integrate the base to make them into absolute patterns + filter + .flat_map(|root| { + root.include.into_iter().flat_map(|it| { + [ + format!("{it}/**/*.rs"), + // FIXME @alibektas : Following dbarsky's recomm I merged toml and lock patterns into one. + // Is this correct? + format!("{it}/**/Cargo.{{toml,lock}}"), + format!("{it}/**/rust-analyzer.toml"), + ] + }) + }) + .map(|glob_pattern| lsp_types::FileSystemWatcher { + glob_pattern: lsp_types::GlobPattern::String(glob_pattern), + kind: None, + }) + .collect() + }; + + watchers.extend( + iter::once(self.config.user_config_path().to_string()) + .chain(iter::once(self.config.root_ratoml_path().to_string())) .map(|glob_pattern| lsp_types::FileSystemWatcher { glob_pattern: lsp_types::GlobPattern::String(glob_pattern), kind: None, }) - .collect() - }; + .collect::>(), + ); let registration_options = lsp_types::DidChangeWatchedFilesRegistrationOptions { watchers }; @@ -531,7 +554,41 @@ impl GlobalState { version: self.vfs_config_version, }); self.source_root_config = project_folders.source_root_config; - self.local_roots_parent_map = self.source_root_config.source_root_parent_map(); + self.local_roots_parent_map = Arc::new(self.source_root_config.source_root_parent_map()); + + let user_config_path = self.config.user_config_path(); + let root_ratoml_path = self.config.root_ratoml_path(); + + { + let vfs = &mut self.vfs.write().0; + let loader = &mut self.loader; + + if vfs.file_id(user_config_path).is_none() { + if let Some(user_cfg_abs) = user_config_path.as_path() { + let contents = loader.handle.load_sync(user_cfg_abs); + vfs.set_file_contents(user_config_path.clone(), contents); + } else { + error!("Non-abs virtual path for user config."); + } + } + + if vfs.file_id(root_ratoml_path).is_none() { + // FIXME @alibektas : Sometimes root_path_ratoml collide with a regular ratoml. + // Although this shouldn't be a problem because everything is mapped to a `FileId`. + // We may want to further think about this. + if let Some(root_ratoml_abs) = root_ratoml_path.as_path() { + let contents = loader.handle.load_sync(root_ratoml_abs); + vfs.set_file_contents(root_ratoml_path.clone(), contents); + } else { + error!("Non-abs virtual path for user config."); + } + } + } + + 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)); self.recreate_crate_graph(cause); diff --git a/crates/rust-analyzer/src/tracing/config.rs b/crates/rust-analyzer/src/tracing/config.rs index f77d98933044..fcdbf6c69497 100644 --- a/crates/rust-analyzer/src/tracing/config.rs +++ b/crates/rust-analyzer/src/tracing/config.rs @@ -13,6 +13,7 @@ use tracing_tree::HierarchicalLayer; use crate::tracing::hprof; +#[derive(Debug)] pub struct Config { pub writer: T, pub filter: String, diff --git a/crates/rust-analyzer/tests/slow-tests/main.rs b/crates/rust-analyzer/tests/slow-tests/main.rs index 8c982b468795..e445b2d5e77e 100644 --- a/crates/rust-analyzer/tests/slow-tests/main.rs +++ b/crates/rust-analyzer/tests/slow-tests/main.rs @@ -17,28 +17,32 @@ mod support; mod testdir; mod tidy; -use std::{collections::HashMap, path::PathBuf, time::Instant}; +use std::{collections::HashMap, path::PathBuf, sync::Once, time::Instant}; use lsp_types::{ - notification::DidOpenTextDocument, + notification::{DidChangeTextDocument, DidOpenTextDocument, DidSaveTextDocument}, request::{ CodeActionRequest, Completion, Formatting, GotoTypeDefinition, HoverRequest, InlayHintRequest, InlayHintResolveRequest, WillRenameFiles, WorkspaceSymbolRequest, }, - CodeActionContext, CodeActionParams, CompletionParams, DidOpenTextDocumentParams, - DocumentFormattingParams, FileRename, FormattingOptions, GotoDefinitionParams, HoverParams, - InlayHint, InlayHintLabel, InlayHintParams, PartialResultParams, Position, Range, - RenameFilesParams, TextDocumentItem, TextDocumentPositionParams, WorkDoneProgressParams, + 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, }; +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}, - testdir::TestDir, -}; +use crate::support::{project, Project}; #[test] fn completes_items_from_standard_library() { @@ -1274,3 +1278,1027 @@ 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/support.rs b/crates/rust-analyzer/tests/slow-tests/support.rs index c6778bf65640..07edf95ddd43 100644 --- a/crates/rust-analyzer/tests/slow-tests/support.rs +++ b/crates/rust-analyzer/tests/slow-tests/support.rs @@ -9,7 +9,10 @@ use crossbeam_channel::{after, select, Receiver}; use lsp_server::{Connection, Message, Notification, Request}; use lsp_types::{notification::Exit, request::Shutdown, TextDocumentIdentifier, Url}; use paths::{Utf8Path, Utf8PathBuf}; -use rust_analyzer::{config::Config, lsp, main_loop}; +use rust_analyzer::{ + config::{Config, ConfigChange, ConfigError}, + lsp, main_loop, +}; use serde::Serialize; use serde_json::{json, to_string_pretty, Value}; use test_utils::FixtureWithProjectMeta; @@ -24,6 +27,7 @@ pub(crate) struct Project<'a> { roots: Vec, config: serde_json::Value, root_dir_contains_symlink: bool, + user_config_path: Option, } impl Project<'_> { @@ -47,9 +51,15 @@ impl Project<'_> { } }), root_dir_contains_symlink: false, + user_config_path: None, } } + pub(crate) fn user_config_dir(mut self, config_path_dir: TestDir) -> Self { + self.user_config_path = Some(config_path_dir.path().to_owned()); + self + } + pub(crate) fn tmp_dir(mut self, tmp_dir: TestDir) -> Self { self.tmp_dir = Some(tmp_dir); self @@ -111,10 +121,17 @@ impl Project<'_> { assert!(proc_macro_names.is_empty()); assert!(mini_core.is_none()); assert!(toolchain.is_none()); + for entry in fixture { - let path = tmp_dir.path().join(&entry.path['/'.len_utf8()..]); - fs::create_dir_all(path.parent().unwrap()).unwrap(); - fs::write(path.as_path(), entry.text.as_bytes()).unwrap(); + if let Some(pth) = entry.path.strip_prefix("/$$CONFIG_DIR$$") { + let path = self.user_config_path.clone().unwrap().join(&pth['/'.len_utf8()..]); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path.as_path(), entry.text.as_bytes()).unwrap(); + } else { + let path = tmp_dir.path().join(&entry.path['/'.len_utf8()..]); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path.as_path(), entry.text.as_bytes()).unwrap(); + } } let tmp_dir_path = AbsPathBuf::assert(tmp_dir.path().to_path_buf()); @@ -184,8 +201,13 @@ impl Project<'_> { }, roots, None, + self.user_config_path, ); - config.update(self.config).expect("invalid config"); + let mut change = ConfigChange::default(); + 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); config.rediscover_workspaces(); Server::new(tmp_dir, config) diff --git a/crates/rust-analyzer/tests/slow-tests/tidy.rs b/crates/rust-analyzer/tests/slow-tests/tidy.rs index 34439391333d..4a7415b016da 100644 --- a/crates/rust-analyzer/tests/slow-tests/tidy.rs +++ b/crates/rust-analyzer/tests/slow-tests/tidy.rs @@ -144,6 +144,7 @@ MIT OR Apache-2.0 MIT OR Apache-2.0 OR Zlib MIT OR Zlib OR Apache-2.0 MIT/Apache-2.0 +MPL-2.0 Unlicense OR MIT Unlicense/MIT Zlib OR Apache-2.0 OR MIT