Unnamed repository; edit this file 'description' to name the repository.
Apply suggested changes
Ali Bektas 2024-06-05
parent 29e5cdf · commit 23a5f31
-rw-r--r--Cargo.lock10
-rw-r--r--crates/ide/src/lib.rs9
-rw-r--r--crates/paths/src/lib.rs18
-rw-r--r--crates/rust-analyzer/Cargo.toml1
-rw-r--r--crates/rust-analyzer/src/bin/main.rs16
-rw-r--r--crates/rust-analyzer/src/cli/scip.rs10
-rw-r--r--crates/rust-analyzer/src/config.rs760
-rw-r--r--crates/rust-analyzer/src/diagnostics.rs2
-rw-r--r--crates/rust-analyzer/src/diagnostics/to_proto.rs1
-rw-r--r--crates/rust-analyzer/src/global_state.rs108
-rw-r--r--crates/rust-analyzer/src/handlers/notification.rs10
-rw-r--r--crates/rust-analyzer/src/handlers/request.rs26
-rw-r--r--crates/rust-analyzer/src/lib.rs2
-rw-r--r--crates/rust-analyzer/src/main_loop.rs5
-rw-r--r--crates/rust-analyzer/src/reload.rs117
-rw-r--r--crates/rust-analyzer/src/tracing/config.rs1
-rw-r--r--crates/rust-analyzer/tests/slow-tests/main.rs1048
-rw-r--r--crates/rust-analyzer/tests/slow-tests/support.rs33
18 files changed, 1878 insertions, 299 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 3558c39bb3..d150c31c48 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -329,6 +329,15 @@ dependencies = [
]
[[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"
@@ -1665,6 +1674,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 431aa30e56..e9408bf897 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<SourceRootId> {
+ pub fn source_root_id(&self, file_id: FileId) -> Cancellable<SourceRootId> {
self.with_db(|db| db.file_source_root(file_id))
}
+ pub fn is_local_source_root(&self, source_root_id: SourceRootId) -> Cancellable<bool> {
+ self.with_db(|db| {
+ let sr = db.source_root(source_root_id);
+ !sr.is_library
+ })
+ }
+
pub fn parallel_prime_caches<F>(&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 1dda02e3f1..7d7cf0220e 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 34b3e49314..8ff7235b8f 100644
--- a/crates/rust-analyzer/Cargo.toml
+++ b/crates/rust-analyzer/Cargo.toml
@@ -22,6 +22,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 9daae914d7..7e58cd70c4 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 aef2c1be22..b2d7056289 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 a8d1e72aed..51664dd799 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.
@@ -250,6 +247,71 @@ config_data! {
/// If false, `-p <package>` 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<usize> = 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<String, SnippetDef> = 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,
+ /// Term search fuel in "units of work" for autocompletion (Defaults to 200).
+ completion_termSearch_fuel: usize = 200,
+
/// List of rust-analyzer diagnostics to disable.
diagnostics_disabled: FxHashSet<String> = FxHashSet::default(),
/// Whether to show native rust-analyzer diagnostics.
@@ -451,76 +513,16 @@ 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 -> {
+ /// 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,
/// Term search fuel in "units of work" for assists (Defaults to 400).
assist_termSearch_fuel: usize = 400,
- /// 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<usize> = 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<String, SnippetDef> = 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,
- /// Term search fuel in "units of work" for autocompletion (Defaults to 200).
- completion_termSearch_fuel: usize = 200,
-
/// Enables highlighting of related references while the cursor is on `break`, `loop`, `while`, or `for` keywords.
highlightRelated_breakPoints_enable: bool = true,
/// Enables highlighting of all captures of a closure while the cursor is on the `|` or move keyword of a closure.
@@ -659,23 +661,304 @@ pub struct Config {
workspace_roots: Vec<AbsPathBuf>,
caps: lsp_types::ClientCapabilities,
root_path: AbsPathBuf,
- detached_files: Vec<AbsPathBuf>,
snippets: Vec<Snippet>,
visual_studio_code_version: Option<Version>,
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<RatomlNode>,
+
+ /// 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<RatomlNode>,
+
+ /// For every `SourceRoot` there can be at most one RATOML file.
ratoml_files: FxHashMap<SourceRootId, RatomlNode>,
+
+ /// Clone of the value that is stored inside a `GlobalState`.
+ source_root_parent_map: Arc<FxHashMap<SourceRootId, SourceRootId>>,
+
+ /// 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<AbsPathBuf>,
+}
+
+impl Serialize for RatomlNode {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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<SourceRootId>,
+ /// Walk towards the root starting from a specified `ConfigNode`
+ fn traverse(
+ &self,
+ start: ConfigNodeKey,
+ ) -> impl Iterator<Item = (ConfigNodeKey, ConfigNodeValue<'_>)> {
+ let mut v = vec![];
+
+ if let ConfigNodeKey::Ratoml(start) = start {
+ let mut par: Option<SourceRootId> = 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::<Vec<Utf8PathBuf>>(
+ &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"),
+ <serde_json::Error as serde::de::Error>::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<serde_json::Value>,
+ ratoml_file_change: Option<FxHashMap<SourceRootId, (FileId, VfsPath, Option<String>)>>,
+ source_map_change: Option<Arc<FxHashMap<SourceRootId, SourceRootId>>>,
+}
+
+impl ConfigChange {
+ pub fn change_ratoml(
+ &mut self,
+ source_root: SourceRootId,
+ file_id: FileId,
+ vfs_path: VfsPath,
+ content: Option<String>,
+ ) -> Option<(FileId, VfsPath, Option<String>)> {
+ 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<FxHashMap<SourceRootId, SourceRootId>>,
+ ) {
+ assert!(self.source_map_change.is_none());
+ self.source_map_change = Some(source_root_map.clone());
+ }
}
macro_rules! try_ {
@@ -866,23 +1149,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<ConfigErrorInner>);
+
+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)
}
}
@@ -894,19 +1191,45 @@ impl Config {
caps: ClientCapabilities,
workspace_roots: Vec<AbsPathBuf>,
visual_studio_code_version: Option<Version>,
+ user_config_path: Option<Utf8PathBuf>,
) -> 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,
}
}
@@ -929,71 +1252,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::<Vec<Utf8PathBuf>>(&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"),
- <serde_json::Error as serde::de::Error>::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()
}
@@ -1002,12 +1260,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
}
}
@@ -1018,7 +1276,7 @@ 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(),
term_search_fuel: self.assist_termSearch_fuel(source_root).to_owned() as u64,
}
@@ -1026,17 +1284,13 @@ impl Config {
pub fn completion(&self, source_root: Option<SourceRootId>) -> 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(),
- enable_term_search: self.completion_termSearch_enable(source_root).to_owned(),
- term_search_fuel: self.completion_termSearch_fuel(source_root).to_owned() as u64,
- 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,
@@ -1055,10 +1309,18 @@ impl Config {
prefer_no_std: self.imports_preferNoStd(source_root).to_owned(),
prefer_prelude: self.imports_preferPrelude(source_root).to_owned(),
snippets: self.snippets.clone().to_vec(),
- limit: self.completion_limit(source_root).to_owned(),
+ limit: self.completion_limit().to_owned(),
+ enable_term_search: self.completion_termSearch_enable().to_owned(),
+ term_search_fuel: self.completion_termSearch_fuel().to_owned() as u64,
}
}
+ pub fn detached_files(&self) -> &Vec<AbsPathBuf> {
+ // 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<SourceRootId>) -> DiagnosticsConfig {
DiagnosticsConfig {
enabled: *self.diagnostics_enable(),
@@ -1066,7 +1328,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,
},
@@ -2016,7 +2278,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<String>,
@@ -2111,7 +2373,7 @@ enum ImportGranularityDef {
#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
-enum CallableCompletionDef {
+pub(crate) enum CallableCompletionDef {
FillArguments,
AddParentheses,
None,
@@ -2318,15 +2580,30 @@ macro_rules! _impl_for_config_data {
$(
$($doc)*
#[allow(non_snake_case)]
- $vis fn $field(&self, _source_root: Option<SourceRootId>) -> &$ty {
- if let Some(v) = self.client_config.local.$field.as_ref() {
- return &v;
+ $vis fn $field(&self, source_root: Option<SourceRootId>) -> &$ty {
+
+ if source_root.is_some() {
+ let mut par: Option<SourceRootId> = 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
}
)*
@@ -2342,14 +2619,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
}
)*
@@ -2502,11 +2788,10 @@ struct DefaultConfigData {
/// All of the config levels, all fields `Option<T>`, 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,
}
@@ -2545,7 +2830,7 @@ impl FullConfigInput {
/// All of the config levels, all fields `Option<T>`, 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,
@@ -3104,12 +3389,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);
}
@@ -3120,12 +3410,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()));
}
@@ -3136,12 +3430,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())
@@ -3155,12 +3456,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())
@@ -3174,12 +3480,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")))
@@ -3193,12 +3504,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 65a9a49149..6dc608c777 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 3d3f944019..4832e8cab4 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 f64e66183d..2210dab0f5 100644
--- a/crates/rust-analyzer/src/global_state.rs
+++ b/crates/rust-analyzer/src/global_state.rs
@@ -25,13 +25,16 @@ use project_model::{
use rustc_hash::{FxHashMap, FxHashSet};
use tracing::{span, Level};
use triomphe::Arc;
-use vfs::{AnchoredPathBuf, Vfs};
+use vfs::{AnchoredPathBuf, 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,
@@ -71,7 +74,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<SourceRootId, SourceRootId>,
+ pub(crate) local_roots_parent_map: Arc<FxHashMap<SourceRootId, SourceRootId>>,
pub(crate) semantic_tokens_cache: Arc<Mutex<FxHashMap<Url, SemanticTokens>>>,
// status
@@ -213,7 +216,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([]),
@@ -254,6 +257,24 @@ impl GlobalState {
pub(crate) fn process_changes(&mut self) -> bool {
let _p = span!(Level::INFO, "GlobalState::process_changes").entered();
+
+ // 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<FileId, vfs::VfsPath> = FxHashMap::default();
+ let mut ratoml_text_map: FxHashMap<FileId, (vfs::VfsPath, Option<String>)> =
+ FxHashMap::default();
+
+ let mut user_config_file: Option<(FileId, Option<String>)> = None;
+ let mut root_path_ratoml: Option<(FileId, Option<String>)> = 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();
@@ -273,6 +294,11 @@ impl GlobalState {
let mut modified_rust_files = vec![];
for file in changed_files.into_values() {
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();
@@ -311,10 +337,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 {
@@ -327,6 +373,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)
+ {
+ // 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);
+ }
+ }
+ }
+ } 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)));
+ }
+
+ 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 9d30063ccc..123a9a06a3 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");
@@ -196,10 +197,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 1e24bf3aae..ac80e7871c 100644
--- a/crates/rust-analyzer/src/handlers/request.rs
+++ b/crates/rust-analyzer/src/handlers/request.rs
@@ -367,7 +367,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, &params.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)?;
@@ -949,7 +949,7 @@ pub(crate) fn handle_completion(
let completion_trigger_character =
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));
// FIXME: We should fix up the position when retrying the cancelled request instead
position.offset = position.offset.min(line_index.index.len());
@@ -997,7 +997,7 @@ pub(crate) fn handle_completion_resolve(
let Ok(offset) = from_proto::offset(&line_index, resolve_data.position.position) else {
return Ok(original_completion);
};
- let source_root = snap.analysis.source_root(file_id)?;
+ let source_root = snap.analysis.source_root_id(file_id)?;
let additional_edits = snap
.analysis
@@ -1229,7 +1229,7 @@ pub(crate) fn handle_code_action(
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
let line_index = snap.file_line_index(file_id)?;
let frange = from_proto::file_range(&snap, &params.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
@@ -1307,7 +1307,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
@@ -1460,7 +1460,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
@@ -1511,7 +1511,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 range = TextRange::new(
range.start().min(line_index.index.len()),
range.end().min(line_index.index.len()),
@@ -1553,7 +1553,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();
@@ -1687,7 +1687,7 @@ pub(crate) fn handle_semantic_tokens_full(
let file_id = from_proto::file_id(&snap, &params.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.
@@ -1718,7 +1718,7 @@ pub(crate) fn handle_semantic_tokens_full_delta(
let file_id = from_proto::file_id(&snap, &params.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.
@@ -1762,7 +1762,7 @@ pub(crate) fn handle_semantic_tokens_range(
let frange = from_proto::file_range(&snap, &params.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.
@@ -1991,8 +1991,8 @@ fn goto_type_action_links(
snap: &GlobalStateSnapshot,
nav_targets: &[HoverGotoTypeData],
) -> Option<lsp_ext::CommandLinkGroup> {
- 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 175ffa622f..d9b31550c5 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 193b3fdd4a..dd2104c9c3 100644
--- a/crates/rust-analyzer/src/main_loop.rs
+++ b/crates/rust-analyzer/src/main_loop.rs
@@ -186,6 +186,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 627be7e951..e804ba3db9 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::{ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, 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,
@@ -443,40 +445,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<FileSystemWatcher> =
+ 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::<Vec<FileSystemWatcher>>(),
+ );
let registration_options =
lsp_types::DidChangeWatchedFilesRegistrationOptions { watchers };
@@ -548,7 +571,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 f77d989330..fcdbf6c694 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<T> {
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 43a8305010..fd6f79abc1 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() {
@@ -1467,3 +1471,1027 @@ version = "0.0.0"
server.request::<WorkspaceSymbolRequest>(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<Url>,
+ 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<serde_json::Value>,
+ ) -> 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::<Vec<_>>();
+ 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::<DidOpenTextDocument>(DidOpenTextDocumentParams {
+ text_document: TextDocumentItem {
+ uri: url.clone(),
+ language_id: "rust".to_owned(),
+ version: 0,
+ text: String::new(),
+ },
+ });
+
+ self.server.notification::<DidChangeTextDocument>(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::<DidOpenTextDocument>(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::<DidSaveTextDocument>(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::<DidOpenTextDocument>(DidOpenTextDocumentParams {
+ text_document: TextDocumentItem {
+ uri: self.urls[file_idx].clone(),
+ language_id: "rust".to_owned(),
+ version: 0,
+ text: String::new(),
+ },
+ });
+
+ self.server.notification::<DidChangeTextDocument>(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::<CodeActionRequest>(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::<CodeActionResponse>(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::<HoverRequest>(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::<Hover>(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::<DidChangeConfiguration>(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 cf27cc7eef..17485ee3ae 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<Utf8PathBuf>,
config: serde_json::Value,
root_dir_contains_symlink: bool,
+ user_config_path: Option<Utf8PathBuf>,
}
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,14 @@ 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.keep(), config)