Unnamed repository; edit this file 'description' to name the repository.
Auto merge of #17058 - alibektas:13529/ratoml, r=Veykril
feat: TOML based config for rust-analyzer > Important > > We don't promise _**any**_ stability with this feature yet, any configs exposed may be removed again, the grouping may change etc. # TOML Based Config for RA This PR ( addresses #13529 and this is a follow-up PR on #16639 ) makes rust-analyzer configurable by configuration files called `rust-analyzer.toml`. Files **must** be named `rust-analyzer.toml`. There is not a strict rule regarding where the files should be placed, but it is recommended to put them near a file that triggers server to start (i.e., `Cargo.{toml,lock}`, `rust-project.json`). ## Configuration Types Previous configuration keys are now split into three different classes. 1. Client keys: These keys only make sense when set by the client (e.g., by setting them in `settings.json` in VSCode). They are but a small portion of this list. One such example is `rust_analyzer.files_watcher`, based on which either the client or the server will be responsible for watching for changes made to project files. 2. Global keys: These keys apply to the entire workspace and can only be set on the very top layers of the hierarchy. The next section gives instructions on which layers these are. 3. Local keys: Keys that can be changed for each crate if desired. ### How Am I Supposed To Know If A Config Is Gl/Loc/Cl ? #17101 ## Configuration Hierarchy There are 5 levels in the configuration hierarchy. When a key is searched for, it is searched in a bottom-up depth-first fashion. ### Default Configuration **Scope**: Global, Local, and Client This is a hard-coded set of configurations. When a configuration key could not be found, then its default value applies. ### User configuration **Scope**: Global, Local If you want your configurations to apply to **every** project you have, you can do so by setting them in your `$CONFIG_DIR/rust-analyzer/rust-analyzer.toml` file, where `$CONFIG_DIR` is : | 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 | ### Client configuration **Scope**: Global, Local, and Client Previously, the only way to configure rust-analyzer was to configure it from the settings of the Client you are using. This level corresponds to that. > With this PR, you don't need to port anything to benefit from new features. You can continue to use your old settings as they are. ### Workspace Root Configuration **Scope**: Global, Local Rust-analyzer already used the path of the workspace you opened in your Client. We used this information to create a configuration file that won't affect your other projects and define global level configurations at the same time. ### Local Configuration **Scope**: Local You can also configure rust-analyzer on a crate level. Although it is not an error to define global ( or client ) level keys in such files, they won't be taken into consideration by the server. Defined local keys will affect the crate in which they are defined and crate's descendants. Internally, a Rust project is split into what we call `SourceRoot`s. This, although with exceptions, is equal to splitting a project into crates. > You may choose to have more than one `rust-analyzer.toml` files within a `SourceRoot`, but among them, the one closer to the project root will be
bors 2024-06-07
parent 26c1638 · parent 5a7bf1d · commit 7c5d496
-rw-r--r--Cargo.lock10
-rw-r--r--crates/ide/src/lib.rs9
-rw-r--r--crates/load-cargo/src/lib.rs11
-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.rs18
-rw-r--r--crates/rust-analyzer/src/cli/scip.rs12
-rw-r--r--crates/rust-analyzer/src/config.rs1257
-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.rs104
-rw-r--r--crates/rust-analyzer/src/handlers/notification.rs17
-rw-r--r--crates/rust-analyzer/src/handlers/request.rs63
-rw-r--r--crates/rust-analyzer/src/lib.rs2
-rw-r--r--crates/rust-analyzer/src/lsp/ext.rs14
-rw-r--r--crates/rust-analyzer/src/main_loop.rs8
-rw-r--r--crates/rust-analyzer/src/reload.rs78
-rw-r--r--crates/rust-analyzer/src/tracing/config.rs1
-rw-r--r--crates/rust-analyzer/tests/slow-tests/main.rs9
-rw-r--r--crates/rust-analyzer/tests/slow-tests/ratoml.rs947
-rw-r--r--crates/rust-analyzer/tests/slow-tests/support.rs35
-rw-r--r--crates/rust-analyzer/tests/slow-tests/tidy.rs21
-rw-r--r--docs/dev/lsp-extensions.md2
23 files changed, 2099 insertions, 541 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 0ff2d53d11..efb313cd3c 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/load-cargo/src/lib.rs b/crates/load-cargo/src/lib.rs
index 8adadddde6..128e86f1d6 100644
--- a/crates/load-cargo/src/lib.rs
+++ b/crates/load-cargo/src/lib.rs
@@ -272,7 +272,7 @@ impl SourceRootConfig {
/// If a `SourceRoot` doesn't have a parent and is local then it is not contained in this mapping but it can be asserted that it is a root `SourceRoot`.
pub fn source_root_parent_map(&self) -> FxHashMap<SourceRootId, SourceRootId> {
let roots = self.fsc.roots();
- let mut map = FxHashMap::<SourceRootId, SourceRootId>::default();
+ let mut i = 0;
roots
.iter()
.enumerate()
@@ -280,17 +280,16 @@ impl SourceRootConfig {
.filter_map(|(idx, (root, root_id))| {
// We are interested in parents if they are also local source roots.
// So instead of a non-local parent we may take a local ancestor as a parent to a node.
- roots.iter().take(idx).find_map(|(root2, root2_id)| {
+ roots[..idx].iter().find_map(|(root2, root2_id)| {
+ i += 1;
if self.local_filesets.contains(root2_id) && root.starts_with(root2) {
return Some((root_id, root2_id));
}
None
})
})
- .for_each(|(child, parent)| {
- map.insert(SourceRootId(*child as u32), SourceRootId(*parent as u32));
- });
- map
+ .map(|(&child, &parent)| (SourceRootId(child as u32), SourceRootId(parent as u32)))
+ .collect()
}
}
diff --git a/crates/paths/src/lib.rs b/crates/paths/src/lib.rs
index 1dda02e3f1..33c3f83db5 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<P: AsRef<Utf8Path>>(&mut self, suffix: P) {
+ 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..774784f37b 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, ConfigErrors},
+ from_json,
+};
use semver::Version;
use tracing_subscriber::fmt::writer::BoxMakeWriter;
use vfs::AbsPathBuf;
@@ -220,16 +224,22 @@ 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 error_sink: ConfigErrors;
+ (config, error_sink, _) = config.apply_change(change);
+
+ 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..8f60b17b59 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,
line_index::{LineEndings, LineIndex, PositionEncoding},
};
@@ -35,12 +37,20 @@ 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 error_sink;
+ (config, error_sink, _) = config.apply_change(change);
+
+ // 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 2574cf059d..64054669df 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 std::{fmt, iter, ops::Not, sync::OnceLock};
use cfg::{CfgAtom, CfgDiff};
+use dirs::config_dir;
use flycheck::{CargoOptions, FlycheckConfig};
use ide::{
AssistConfig, CallableSnippets, CompletionConfig, DiagnosticsConfig, ExprFillDefaultMode,
@@ -29,9 +27,13 @@ use project_model::{
};
use rustc_hash::{FxHashMap, FxHashSet};
use semver::Version;
-use serde::{de::DeserializeOwned, Deserialize, Serialize};
+use serde::{
+ de::{DeserializeOwned, Error},
+ Deserialize, Serialize,
+};
use stdx::format_to_acc;
-use vfs::{AbsPath, AbsPathBuf};
+use triomphe::Arc;
+use vfs::{AbsPath, AbsPathBuf, VfsPath};
use crate::{
caps::completion_item_edit_resolve,
@@ -60,6 +62,7 @@ mod patch_old_style;
// parsing the old name.
config_data! {
/// Configs that apply on a workspace-wide scope. There are 3 levels on which a global configuration can be configured
+ // FIXME: 1. and 3. should be split, some configs do not make sense per project
///
/// 1. `rust-analyzer.toml` file under user's config directory (e.g ~/.config/rust-analyzer.toml)
/// 2. Client's own configurations (e.g `settings.json` on VS Code)
@@ -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.
@@ -272,87 +269,12 @@ config_data! {
/// The warnings will be indicated by a blue squiggly underline in code
/// and a blue icon in the `Problems Panel`.
diagnostics_warningsAsInfo: Vec<String> = vec![],
+
/// These directories will be ignored by rust-analyzer. They are
/// relative to the workspace root, and globs are not supported. You may
/// also need to add the folders to Code's `files.watcherExclude`.
files_excludeDirs: Vec<Utf8PathBuf> = vec![],
- /// Controls file watching implementation.
- files_watcher: FilesWatcherDef = FilesWatcherDef::Client,
-
- /// Whether to show `Debug` action. Only applies when
- /// `#rust-analyzer.hover.actions.enable#` is set.
- hover_actions_debug_enable: bool = true,
- /// Whether to show HoverActions in Rust files.
- hover_actions_enable: bool = true,
- /// Whether to show `Go to Type Definition` action. Only applies when
- /// `#rust-analyzer.hover.actions.enable#` is set.
- hover_actions_gotoTypeDef_enable: bool = true,
- /// Whether to show `Implementations` action. Only applies when
- /// `#rust-analyzer.hover.actions.enable#` is set.
- hover_actions_implementations_enable: bool = true,
- /// Whether to show `References` action. Only applies when
- /// `#rust-analyzer.hover.actions.enable#` is set.
- hover_actions_references_enable: bool = false,
- /// Whether to show `Run` action. Only applies when
- /// `#rust-analyzer.hover.actions.enable#` is set.
- hover_actions_run_enable: bool = true,
-
- /// Whether to show documentation on hover.
- hover_documentation_enable: bool = true,
- /// Whether to show keyword hover popups. Only applies when
- /// `#rust-analyzer.hover.documentation.enable#` is set.
- hover_documentation_keywords_enable: bool = true,
- /// Use markdown syntax for links on hover.
- hover_links_enable: bool = true,
- /// How to render the align information in a memory layout hover.
- hover_memoryLayout_alignment: Option<MemoryLayoutHoverRenderKindDef> = Some(MemoryLayoutHoverRenderKindDef::Hexadecimal),
- /// Whether to show memory layout data on hover.
- hover_memoryLayout_enable: bool = true,
- /// How to render the niche information in a memory layout hover.
- hover_memoryLayout_niches: Option<bool> = Some(false),
- /// How to render the offset information in a memory layout hover.
- hover_memoryLayout_offset: Option<MemoryLayoutHoverRenderKindDef> = Some(MemoryLayoutHoverRenderKindDef::Hexadecimal),
- /// How to render the size information in a memory layout hover.
- hover_memoryLayout_size: Option<MemoryLayoutHoverRenderKindDef> = Some(MemoryLayoutHoverRenderKindDef::Both),
-
- /// How many variants of an enum to display when hovering on. Show none if empty.
- hover_show_enumVariants: Option<usize> = Some(5),
- /// How many fields of a struct, variant or union to display when hovering on. Show none if empty.
- hover_show_fields: Option<usize> = Some(5),
- /// How many associated items of a trait to display when hovering a trait.
- hover_show_traitAssocItems: Option<usize> = None,
- /// Enables the experimental support for interpreting tests.
- interpret_tests: bool = false,
-
- /// Whether to show `Debug` lens. Only applies when
- /// `#rust-analyzer.lens.enable#` is set.
- lens_debug_enable: bool = true,
- /// Whether to show CodeLens in Rust files.
- lens_enable: bool = true,
- /// Internal config: use custom client-side commands even when the
- /// client doesn't set the corresponding capability.
- lens_forceCustomCommands: bool = true,
- /// Whether to show `Implementations` lens. Only applies when
- /// `#rust-analyzer.lens.enable#` is set.
- lens_implementations_enable: bool = true,
- /// Where to render annotations.
- lens_location: AnnotationLocation = AnnotationLocation::AboveName,
- /// Whether to show `References` lens for Struct, Enum, and Union.
- /// Only applies when `#rust-analyzer.lens.enable#` is set.
- lens_references_adt_enable: bool = false,
- /// Whether to show `References` lens for Enum Variants.
- /// Only applies when `#rust-analyzer.lens.enable#` is set.
- lens_references_enumVariant_enable: bool = false,
- /// Whether to show `Method References` lens. Only applies when
- /// `#rust-analyzer.lens.enable#` is set.
- lens_references_method_enable: bool = false,
- /// Whether to show `References` lens for Trait.
- /// Only applies when `#rust-analyzer.lens.enable#` is set.
- lens_references_trait_enable: bool = false,
- /// Whether to show `Run` lens. Only applies when
- /// `#rust-analyzer.lens.enable#` is set.
- lens_run_enable: bool = true,
/// Disable project auto-discovery in favor of explicitly specified set
/// of projects.
@@ -367,31 +289,10 @@ config_data! {
/// Sets the LRU capacity of the specified queries.
lru_query_capacities: FxHashMap<Box<str>, usize> = FxHashMap::default(),
- /// Whether to show `can't find Cargo.toml` error message.
- notifications_cargoTomlNotFound: bool = true,
-
- /// Whether to send an UnindexedProject notification to the client.
- notifications_unindexedProject: bool = false,
-
- /// How many worker threads in the main loop. The default `null` means to pick automatically.
- numThreads: Option<usize> = None,
-
- /// Expand attribute macros. Requires `#rust-analyzer.procMacro.enable#` to be set.
- procMacro_attributes_enable: bool = true,
- /// Enable support for procedural macros, implies `#rust-analyzer.cargo.buildScripts.enable#`.
- procMacro_enable: bool = true,
/// These proc-macros will be ignored when trying to expand them.
///
/// This config takes a map of crate names with the exported proc-macro names to ignore as values.
procMacro_ignored: FxHashMap<Box<str>, Box<[Box<str>]>> = FxHashMap::default(),
- /// Internal config, path to proc-macro server executable.
- procMacro_server: Option<Utf8PathBuf> = None,
-
- /// Exclude imports from find-all-references.
- references_excludeImports: bool = false,
-
- /// Exclude tests from find-all-references.
- references_excludeTests: bool = false,
/// Command to be executed instead of 'cargo' for runnables.
runnables_command: Option<String> = None,
@@ -429,34 +330,41 @@ config_data! {
/// `textDocument/rangeFormatting` request. The rustfmt option is unstable and only
/// available on a nightly build.
rustfmt_rangeFormatting_enable: bool = false,
-
-
- /// Show full signature of the callable. Only shows parameters if disabled.
- signatureInfo_detail: SignatureDetail = SignatureDetail::Full,
- /// Show documentation.
- signatureInfo_documentation_enable: bool = true,
-
- /// Whether to insert closing angle brackets when typing an opening angle bracket of a generic argument list.
- typing_autoClosingAngleBrackets_enable: bool = false,
-
- /// Workspace symbol search kind.
- workspace_symbol_search_kind: WorkspaceSymbolSearchKindDef = WorkspaceSymbolSearchKindDef::OnlyTypes,
- /// Limits the number of items returned from a workspace symbol search (Defaults to 128).
- /// Some clients like vs-code issue new searches on result filtering and don't require all results to be returned in the initial search.
- /// Other clients requires all results upfront and might require a higher limit.
- workspace_symbol_search_limit: usize = 128,
- /// Workspace symbol search scope.
- workspace_symbol_search_scope: WorkspaceSymbolSearchScopeDef = WorkspaceSymbolSearchScopeDef::Workspace,
}
}
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,
+ /// Whether to enforce the import granularity setting for all files. If set to false rust-analyzer will try to keep import styles consistent per file.
+ imports_granularity_enforce: bool = false,
+ /// How imports should be grouped into use statements.
+ imports_granularity_group: ImportGranularityDef = ImportGranularityDef::Crate,
+ /// Group inserted imports by the https://rust-analyzer.github.io/manual.html#auto-import[following order]. Groups are separated by newlines.
+ imports_group_enable: bool = true,
+ /// Whether to allow import insertion to merge new imports into single path glob imports like `use std::fmt::*;`.
+ imports_merge_glob: bool = true,
+ /// Prefer to unconditionally use imports of the core and alloc crate, over the std crate.
+ imports_preferNoStd | imports_prefer_no_std: bool = false,
+ /// Whether to prefer import paths containing a `prelude` module.
+ imports_preferPrelude: bool = false,
+ /// The path structure for newly inserted paths to use.
+ imports_prefix: ImportPrefixDef = ImportPrefixDef::Plain,
+ }
+}
+
+config_data! {
+ /// Configs that only make sense when they are set by a client. As such they can only be defined
+ /// by setting them using client's settings (e.g `settings.json` on VS Code).
+ client: struct ClientDefaultConfigData <- ClientConfigInput -> {
/// 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,
@@ -521,6 +429,9 @@ config_data! {
/// Term search fuel in "units of work" for autocompletion (Defaults to 200).
completion_termSearch_fuel: usize = 200,
+ /// Controls file watching implementation.
+ files_watcher: FilesWatcherDef = FilesWatcherDef::Client,
+
/// 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.
@@ -532,21 +443,48 @@ config_data! {
/// Enables highlighting of all break points for a loop or block context while the cursor is on any `async` or `await` keywords.
highlightRelated_yieldPoints_enable: bool = true,
- /// Whether to enforce the import granularity setting for all files. If set to false rust-analyzer will try to keep import styles consistent per file.
- imports_granularity_enforce: bool = false,
- /// How imports should be grouped into use statements.
- imports_granularity_group: ImportGranularityDef = ImportGranularityDef::Crate,
- /// Group inserted imports by the https://rust-analyzer.github.io/manual.html#auto-import[following order]. Groups are separated by newlines.
- imports_group_enable: bool = true,
- /// Whether to allow import insertion to merge new imports into single path glob imports like `use std::fmt::*;`.
- imports_merge_glob: bool = true,
- /// Prefer to unconditionally use imports of the core and alloc crate, over the std crate.
- imports_preferNoStd | imports_prefer_no_std: bool = false,
- /// Whether to prefer import paths containing a `prelude` module.
- imports_preferPrelude: bool = false,
- /// The path structure for newly inserted paths to use.
- imports_prefix: ImportPrefixDef = ImportPrefixDef::Plain,
+ /// Whether to show `Debug` action. Only applies when
+ /// `#rust-analyzer.hover.actions.enable#` is set.
+ hover_actions_debug_enable: bool = true,
+ /// Whether to show HoverActions in Rust files.
+ hover_actions_enable: bool = true,
+ /// Whether to show `Go to Type Definition` action. Only applies when
+ /// `#rust-analyzer.hover.actions.enable#` is set.
+ hover_actions_gotoTypeDef_enable: bool = true,
+ /// Whether to show `Implementations` action. Only applies when
+ /// `#rust-analyzer.hover.actions.enable#` is set.
+ hover_actions_implementations_enable: bool = true,
+ /// Whether to show `References` action. Only applies when
+ /// `#rust-analyzer.hover.actions.enable#` is set.
+ hover_actions_references_enable: bool = false,
+ /// Whether to show `Run` action. Only applies when
+ /// `#rust-analyzer.hover.actions.enable#` is set.
+ hover_actions_run_enable: bool = true,
+ /// Whether to show documentation on hover.
+ hover_documentation_enable: bool = true,
+ /// Whether to show keyword hover popups. Only applies when
+ /// `#rust-analyzer.hover.documentation.enable#` is set.
+ hover_documentation_keywords_enable: bool = true,
+ /// Use markdown syntax for links on hover.
+ hover_links_enable: bool = true,
+ /// How to render the align information in a memory layout hover.
+ hover_memoryLayout_alignment: Option<MemoryLayoutHoverRenderKindDef> = Some(MemoryLayoutHoverRenderKindDef::Hexadecimal),
+ /// Whether to show memory layout data on hover.
+ hover_memoryLayout_enable: bool = true,
+ /// How to render the niche information in a memory layout hover.
+ hover_memoryLayout_niches: Option<bool> = Some(false),
+ /// How to render the offset information in a memory layout hover.
+ hover_memoryLayout_offset: Option<MemoryLayoutHoverRenderKindDef> = Some(MemoryLayoutHoverRenderKindDef::Hexadecimal),
+ /// How to render the size information in a memory layout hover.
+ hover_memoryLayout_size: Option<MemoryLayoutHoverRenderKindDef> = Some(MemoryLayoutHoverRenderKindDef::Both),
+
+ /// How many variants of an enum to display when hovering on. Show none if empty.
+ hover_show_enumVariants: Option<usize> = Some(5),
+ /// How many fields of a struct, variant or union to display when hovering on. Show none if empty.
+ hover_show_fields: Option<usize> = Some(5),
+ /// How many associated items of a trait to display when hovering a trait.
+ hover_show_traitAssocItems: Option<usize> = None,
/// Whether to show inlay type hints for binding modes.
inlayHints_bindingModeHints_enable: bool = false,
@@ -597,6 +535,8 @@ config_data! {
/// Whether to hide inlay type hints for constructors.
inlayHints_typeHints_hideNamedConstructor: bool = false,
+ /// Enables the experimental support for interpreting tests.
+ interpret_tests: bool = false,
/// Join lines merges consecutive declaration and initialization of an assignment.
joinLines_joinAssignments: bool = true,
@@ -607,6 +547,57 @@ config_data! {
/// Join lines unwraps trivial blocks.
joinLines_unwrapTrivialBlock: bool = true,
+ /// Whether to show `Debug` lens. Only applies when
+ /// `#rust-analyzer.lens.enable#` is set.
+ lens_debug_enable: bool = true,
+ /// Whether to show CodeLens in Rust files.
+ lens_enable: bool = true,
+ /// Internal config: use custom client-side commands even when the
+ /// client doesn't set the corresponding capability.
+ lens_forceCustomCommands: bool = true,
+ /// Whether to show `Implementations` lens. Only applies when
+ /// `#rust-analyzer.lens.enable#` is set.
+ lens_implementations_enable: bool = true,
+ /// Where to render annotations.
+ lens_location: AnnotationLocation = AnnotationLocation::AboveName,
+ /// Whether to show `References` lens for Struct, Enum, and Union.
+ /// Only applies when `#rust-analyzer.lens.enable#` is set.
+ lens_references_adt_enable: bool = false,
+ /// Whether to show `References` lens for Enum Variants.
+ /// Only applies when `#rust-analyzer.lens.enable#` is set.
+ lens_references_enumVariant_enable: bool = false,
+ /// Whether to show `Method References` lens. Only applies when
+ /// `#rust-analyzer.lens.enable#` is set.
+ lens_references_method_enable: bool = false,
+ /// Whether to show `References` lens for Trait.
+ /// Only applies when `#rust-analyzer.lens.enable#` is set.
+ lens_references_trait_enable: bool = false,
+ /// Whether to show `Run` lens. Only applies when
+ /// `#rust-analyzer.lens.enable#` is set.
+ lens_run_enable: bool = true,
+
+ /// Whether to show `can't find Cargo.toml` error message.
+ notifications_cargoTomlNotFound: bool = true,
+
+ /// Whether to send an UnindexedProject notification to the client.
+ notifications_unindexedProject: bool = false,
+
+ /// How many worker threads in the main loop. The default `null` means to pick automatically.
+ numThreads: Option<usize> = None,
+
+ /// Expand attribute macros. Requires `#rust-analyzer.procMacro.enable#` to be set.
+ procMacro_attributes_enable: bool = true,
+ /// Enable support for procedural macros, implies `#rust-analyzer.cargo.buildScripts.enable#`.
+ procMacro_enable: bool = true,
+ /// Internal config, path to proc-macro server executable.
+ procMacro_server: Option<Utf8PathBuf> = None,
+
+ /// Exclude imports from find-all-references.
+ references_excludeImports: bool = false,
+
+ /// Exclude tests from find-all-references.
+ references_excludeTests: bool = false,
+
/// Inject additional highlighting into doc comments.
///
/// When enabled, rust-analyzer will highlight rust source in doc comments as well as intra
@@ -643,13 +634,24 @@ config_data! {
/// By disabling semantic tokens for strings, other grammars can be used to highlight
/// their contents.
semanticHighlighting_strings_enable: bool = true,
- }
-}
-config_data! {
- /// Configs that only make sense when they are set by a client. As such they can only be defined
- /// by setting them using client's settings (e.g `settings.json` on VS Code).
- client: struct ClientDefaultConfigData <- ClientConfigInput -> {}
+ /// Show full signature of the callable. Only shows parameters if disabled.
+ signatureInfo_detail: SignatureDetail = SignatureDetail::Full,
+ /// Show documentation.
+ signatureInfo_documentation_enable: bool = true,
+
+ /// Whether to insert closing angle brackets when typing an opening angle bracket of a generic argument list.
+ typing_autoClosingAngleBrackets_enable: bool = false,
+
+ /// Workspace symbol search kind.
+ workspace_symbol_search_kind: WorkspaceSymbolSearchKindDef = WorkspaceSymbolSearchKindDef::OnlyTypes,
+ /// Limits the number of items returned from a workspace symbol search (Defaults to 128).
+ /// Some clients like vs-code issue new searches on result filtering and don't require all results to be returned in the initial search.
+ /// Other clients requires all results upfront and might require a higher limit.
+ workspace_symbol_search_limit: usize = 128,
+ /// Workspace symbol search scope.
+ workspace_symbol_search_scope: WorkspaceSymbolSearchScopeDef = WorkspaceSymbolSearchScopeDef::Workspace,
+ }
}
#[derive(Debug, Clone)]
@@ -659,23 +661,294 @@ 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)]
- ratoml_files: FxHashMap<SourceRootId, RatomlNode>,
+ default_config: &'static DefaultConfigData,
+ /// Config node that obtains its initial value during the server initialization and
+ /// by receiving a `lsp_types::notification::DidChangeConfiguration`.
+ client_config: (FullConfigInput, ConfigErrors),
+
+ /// 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<(GlobalLocalConfigInput, ConfigErrors)>,
+
+ /// 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<(GlobalLocalConfigInput, ConfigErrors)>,
+
+ /// For every `SourceRoot` there can be at most one RATOML file.
+ ratoml_files: FxHashMap<SourceRootId, (LocalConfigInput, ConfigErrors)>,
+
+ /// Clone of the value that is stored inside a `GlobalState`.
+ source_root_parent_map: Arc<FxHashMap<SourceRootId, SourceRootId>>,
+
+ detached_files: Vec<AbsPathBuf>,
}
-#[derive(Clone, Debug)]
-struct RatomlNode {
- #[allow(dead_code)]
- node: GlobalLocalConfigInput,
- #[allow(dead_code)]
- parent: Option<SourceRootId>,
+impl Config {
+ pub fn user_config_path(&self) -> &VfsPath {
+ &self.user_config_path
+ }
+
+ pub fn same_source_root_parent_map(
+ &self,
+ other: &Arc<FxHashMap<SourceRootId, SourceRootId>>,
+ ) -> bool {
+ Arc::ptr_eq(&self.source_root_parent_map, other)
+ }
+
+ // FIXME @alibektas : Server's health uses error sink but in other places it is not used atm.
+ /// Changes made to client and global configurations will partially not be reflected even after `.apply_change()` was called.
+ /// The return tuple's bool component signals whether the `GlobalState` should call its `update_configuration()` method.
+ fn apply_change_with_sink(&self, change: ConfigChange) -> (Config, bool) {
+ let mut config = self.clone();
+
+ let mut should_update = false;
+
+ if let Some(change) = change.user_config_change {
+ if let Ok(table) = toml::from_str(&change) {
+ let mut toml_errors = vec![];
+ validate_toml_table(
+ GlobalLocalConfigInput::FIELDS,
+ &table,
+ &mut String::new(),
+ &mut toml_errors,
+ );
+ config.user_config = Some((
+ GlobalLocalConfigInput::from_toml(table, &mut toml_errors),
+ ConfigErrors(
+ toml_errors
+ .into_iter()
+ .map(|(a, b)| ConfigErrorInner::Toml { config_key: a, error: b })
+ .map(Arc::new)
+ .collect(),
+ ),
+ ));
+ 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 mut json_errors = vec![];
+ 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 = (
+ FullConfigInput::from_json(json, &mut json_errors),
+ ConfigErrors(
+ json_errors
+ .into_iter()
+ .map(|(a, b)| ConfigErrorInner::Json { config_key: a, error: b })
+ .map(Arc::new)
+ .collect(),
+ ),
+ );
+ config.detached_files = detached_files;
+ }
+ should_update = true;
+ }
+
+ if let Some(change) = change.root_ratoml_change {
+ tracing::info!("updating root ra-toml config: {:#}", change);
+ #[allow(clippy::single_match)]
+ match toml::from_str(&change) {
+ Ok(table) => {
+ let mut toml_errors = vec![];
+ validate_toml_table(
+ GlobalLocalConfigInput::FIELDS,
+ &table,
+ &mut String::new(),
+ &mut toml_errors,
+ );
+ config.root_ratoml = Some((
+ GlobalLocalConfigInput::from_toml(table, &mut toml_errors),
+ ConfigErrors(
+ toml_errors
+ .into_iter()
+ .map(|(a, b)| ConfigErrorInner::Toml { config_key: a, error: b })
+ .map(Arc::new)
+ .collect(),
+ ),
+ ));
+ should_update = true;
+ }
+ // FIXME
+ Err(_) => (),
+ }
+ }
+
+ if let Some(change) = change.ratoml_file_change {
+ for (source_root_id, (_, text)) in change {
+ if let Some(text) = text {
+ let mut toml_errors = vec![];
+ tracing::info!("updating ra-toml config: {:#}", text);
+ #[allow(clippy::single_match)]
+ match toml::from_str(&text) {
+ Ok(table) => {
+ validate_toml_table(
+ &[LocalConfigInput::FIELDS],
+ &table,
+ &mut String::new(),
+ &mut toml_errors,
+ );
+ config.ratoml_files.insert(
+ source_root_id,
+ (
+ LocalConfigInput::from_toml(&table, &mut toml_errors),
+ ConfigErrors(
+ toml_errors
+ .into_iter()
+ .map(|(a, b)| ConfigErrorInner::Toml {
+ config_key: a,
+ error: b,
+ })
+ .map(Arc::new)
+ .collect(),
+ ),
+ ),
+ );
+ }
+ // FIXME
+ Err(_) => (),
+ }
+ }
+ }
+ }
+
+ 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,
+ };
+ #[allow(clippy::single_match)]
+ 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),
+ // FIXME
+ // None => error_sink.0.push(ConfigErrorInner::Json {
+ // config_key: "".to_owned(),
+ // error: <serde_json::Error as serde::de::Error>::custom(format!(
+ // "snippet {name} is invalid or triggers are missing",
+ // )),
+ // }),
+ None => (),
+ }
+ }
+
+ // FIXME: bring this back
+ // if config.check_command().is_empty() {
+ // error_sink.0.push(ConfigErrorInner::Json {
+ // config_key: "/check/command".to_owned(),
+ // error: serde_json::Error::custom("expected a non-empty string"),
+ // });
+ // }
+ (config, should_update)
+ }
+
+ /// Given `change` this generates a new `Config`, thereby collecting errors of type `ConfigError`.
+ /// If there are changes that have global/client level effect, the last component of the return type
+ /// will be set to `true`, which should be used by the `GlobalState` to update itself.
+ pub fn apply_change(&self, change: ConfigChange) -> (Config, ConfigErrors, bool) {
+ let (config, should_update) = self.apply_change_with_sink(change);
+ let e = ConfigErrors(
+ config
+ .client_config
+ .1
+ .0
+ .iter()
+ .chain(config.root_ratoml.as_ref().into_iter().flat_map(|it| it.1 .0.iter()))
+ .chain(config.user_config.as_ref().into_iter().flat_map(|it| it.1 .0.iter()))
+ .chain(config.ratoml_files.values().flat_map(|it| it.1 .0.iter()))
+ .cloned()
+ .collect(),
+ );
+ (config, e, should_update)
+ }
+}
+
+#[derive(Default, Debug)]
+pub struct ConfigChange {
+ user_config_change: Option<Arc<str>>,
+ root_ratoml_change: Option<Arc<str>>,
+ client_config_change: Option<serde_json::Value>,
+ ratoml_file_change: Option<FxHashMap<SourceRootId, (VfsPath, Option<Arc<str>>)>>,
+ source_map_change: Option<Arc<FxHashMap<SourceRootId, SourceRootId>>>,
+}
+
+impl ConfigChange {
+ pub fn change_ratoml(
+ &mut self,
+ source_root: SourceRootId,
+ vfs_path: VfsPath,
+ content: Option<Arc<str>>,
+ ) -> Option<(VfsPath, Option<Arc<str>>)> {
+ self.ratoml_file_change
+ .get_or_insert_with(Default::default)
+ .insert(source_root, (vfs_path, content))
+ }
+
+ pub fn change_user_config(&mut self, content: Option<Arc<str>>) {
+ 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<Arc<str>>) {
+ assert!(self.root_ratoml_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,27 +1139,39 @@ pub struct ClientCommandsConfig {
}
#[derive(Debug)]
-pub struct ConfigError {
- errors: Vec<(String, serde_json::Error)>,
+pub enum ConfigErrorInner {
+ Json { config_key: String, error: serde_json::Error },
+ Toml { config_key: String, error: toml::de::Error },
}
-impl fmt::Display for ConfigError {
+#[derive(Clone, Debug)]
+pub struct ConfigErrors(Vec<Arc<ConfigErrorInner>>);
+
+impl ConfigErrors {
+ pub fn is_empty(&self) -> bool {
+ self.0.is_empty()
+ }
+}
+
+impl fmt::Display for ConfigErrors {
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::Json { config_key: key, error: e } => {
+ f(key)?;
+ f(&": ")?;
+ f(e)
+ }
+ ConfigErrorInner::Toml { config_key: key, error: 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)
}
}
-impl std::error::Error for ConfigError {}
+impl std::error::Error for ConfigErrors {}
impl Config {
pub fn new(
@@ -894,19 +1179,46 @@ impl Config {
caps: ClientCapabilities,
workspace_roots: Vec<AbsPathBuf>,
visual_studio_code_version: Option<Version>,
+ user_config_path: Option<Utf8PathBuf>,
) -> Self {
+ static DEFAULT_CONFIG_DATA: OnceLock<&'static DefaultConfigData> = OnceLock::new();
+ 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: (FullConfigInput::default(), ConfigErrors(vec![])),
ratoml_files: FxHashMap::default(),
- default_config: DefaultConfigData::default(),
+ default_config: DEFAULT_CONFIG_DATA.get_or_init(|| Box::leak(Box::default())),
+ source_root_parent_map: Arc::new(FxHashMap::default()),
+ user_config: None,
+ user_config_path,
+ root_ratoml: None,
+ root_ratoml_path,
+ detached_files: Default::default(),
}
}
@@ -929,71 +1241,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 +1249,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 +1265,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 +1273,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 +1298,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.detached_files
+ }
+
pub fn diagnostics(&self, source_root: Option<SourceRootId>) -> DiagnosticsConfig {
DiagnosticsConfig {
enabled: *self.diagnostics_enable(),
@@ -1066,7 +1317,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,
},
@@ -1081,13 +1332,13 @@ impl Config {
self.procMacro_enable().to_owned() && self.procMacro_attributes_enable().to_owned()
}
- pub fn highlight_related(&self, source_root: Option<SourceRootId>) -> HighlightRelatedConfig {
+ pub fn highlight_related(&self, _source_root: Option<SourceRootId>) -> HighlightRelatedConfig {
HighlightRelatedConfig {
- references: self.highlightRelated_references_enable(source_root).to_owned(),
- break_points: self.highlightRelated_breakPoints_enable(source_root).to_owned(),
- exit_points: self.highlightRelated_exitPoints_enable(source_root).to_owned(),
- yield_points: self.highlightRelated_yieldPoints_enable(source_root).to_owned(),
- closure_captures: self.highlightRelated_closureCaptures_enable(source_root).to_owned(),
+ references: self.highlightRelated_references_enable().to_owned(),
+ break_points: self.highlightRelated_breakPoints_enable().to_owned(),
+ exit_points: self.highlightRelated_exitPoints_enable().to_owned(),
+ yield_points: self.highlightRelated_yieldPoints_enable().to_owned(),
+ closure_captures: self.highlightRelated_closureCaptures_enable().to_owned(),
}
}
@@ -1141,7 +1392,7 @@ impl Config {
}
}
- pub fn inlay_hints(&self, source_root: Option<SourceRootId>) -> InlayHintsConfig {
+ pub fn inlay_hints(&self) -> InlayHintsConfig {
let client_capability_fields = self
.caps
.text_document
@@ -1155,74 +1406,65 @@ impl Config {
.collect::<FxHashSet<_>>();
InlayHintsConfig {
- render_colons: self.inlayHints_renderColons(source_root).to_owned(),
- type_hints: self.inlayHints_typeHints_enable(source_root).to_owned(),
- parameter_hints: self.inlayHints_parameterHints_enable(source_root).to_owned(),
- chaining_hints: self.inlayHints_chainingHints_enable(source_root).to_owned(),
- discriminant_hints: match self.inlayHints_discriminantHints_enable(source_root) {
+ render_colons: self.inlayHints_renderColons().to_owned(),
+ type_hints: self.inlayHints_typeHints_enable().to_owned(),
+ parameter_hints: self.inlayHints_parameterHints_enable().to_owned(),
+ chaining_hints: self.inlayHints_chainingHints_enable().to_owned(),
+ discriminant_hints: match self.inlayHints_discriminantHints_enable() {
DiscriminantHintsDef::Always => ide::DiscriminantHints::Always,
DiscriminantHintsDef::Never => ide::DiscriminantHints::Never,
DiscriminantHintsDef::Fieldless => ide::DiscriminantHints::Fieldless,
},
- closure_return_type_hints: match self
- .inlayHints_closureReturnTypeHints_enable(source_root)
- {
+ closure_return_type_hints: match self.inlayHints_closureReturnTypeHints_enable() {
ClosureReturnTypeHintsDef::Always => ide::ClosureReturnTypeHints::Always,
ClosureReturnTypeHintsDef::Never => ide::ClosureReturnTypeHints::Never,
ClosureReturnTypeHintsDef::WithBlock => ide::ClosureReturnTypeHints::WithBlock,
},
- lifetime_elision_hints: match self.inlayHints_lifetimeElisionHints_enable(source_root) {
+ lifetime_elision_hints: match self.inlayHints_lifetimeElisionHints_enable() {
LifetimeElisionDef::Always => ide::LifetimeElisionHints::Always,
LifetimeElisionDef::Never => ide::LifetimeElisionHints::Never,
LifetimeElisionDef::SkipTrivial => ide::LifetimeElisionHints::SkipTrivial,
},
hide_named_constructor_hints: self
- .inlayHints_typeHints_hideNamedConstructor(source_root)
+ .inlayHints_typeHints_hideNamedConstructor()
.to_owned(),
hide_closure_initialization_hints: self
- .inlayHints_typeHints_hideClosureInitialization(source_root)
+ .inlayHints_typeHints_hideClosureInitialization()
.to_owned(),
- closure_style: match self.inlayHints_closureStyle(source_root) {
+ closure_style: match self.inlayHints_closureStyle() {
ClosureStyle::ImplFn => hir::ClosureStyle::ImplFn,
ClosureStyle::RustAnalyzer => hir::ClosureStyle::RANotation,
ClosureStyle::WithId => hir::ClosureStyle::ClosureWithId,
ClosureStyle::Hide => hir::ClosureStyle::Hide,
},
- closure_capture_hints: self
- .inlayHints_closureCaptureHints_enable(source_root)
- .to_owned(),
- adjustment_hints: match self.inlayHints_expressionAdjustmentHints_enable(source_root) {
+ closure_capture_hints: self.inlayHints_closureCaptureHints_enable().to_owned(),
+ adjustment_hints: match self.inlayHints_expressionAdjustmentHints_enable() {
AdjustmentHintsDef::Always => ide::AdjustmentHints::Always,
- AdjustmentHintsDef::Never => {
- match self.inlayHints_reborrowHints_enable(source_root) {
- ReborrowHintsDef::Always | ReborrowHintsDef::Mutable => {
- ide::AdjustmentHints::ReborrowOnly
- }
- ReborrowHintsDef::Never => ide::AdjustmentHints::Never,
+ AdjustmentHintsDef::Never => match self.inlayHints_reborrowHints_enable() {
+ ReborrowHintsDef::Always | ReborrowHintsDef::Mutable => {
+ ide::AdjustmentHints::ReborrowOnly
}
- }
+ ReborrowHintsDef::Never => ide::AdjustmentHints::Never,
+ },
AdjustmentHintsDef::Reborrow => ide::AdjustmentHints::ReborrowOnly,
},
- adjustment_hints_mode: match self.inlayHints_expressionAdjustmentHints_mode(source_root)
- {
+ adjustment_hints_mode: match self.inlayHints_expressionAdjustmentHints_mode() {
AdjustmentHintsModeDef::Prefix => ide::AdjustmentHintsMode::Prefix,
AdjustmentHintsModeDef::Postfix => ide::AdjustmentHintsMode::Postfix,
AdjustmentHintsModeDef::PreferPrefix => ide::AdjustmentHintsMode::PreferPrefix,
AdjustmentHintsModeDef::PreferPostfix => ide::AdjustmentHintsMode::PreferPostfix,
},
adjustment_hints_hide_outside_unsafe: self
- .inlayHints_expressionAdjustmentHints_hideOutsideUnsafe(source_root)
+ .inlayHints_expressionAdjustmentHints_hideOutsideUnsafe()
.to_owned(),
- binding_mode_hints: self.inlayHints_bindingModeHints_enable(source_root).to_owned(),
+ binding_mode_hints: self.inlayHints_bindingModeHints_enable().to_owned(),
param_names_for_lifetime_elision_hints: self
- .inlayHints_lifetimeElisionHints_useParameterNames(source_root)
+ .inlayHints_lifetimeElisionHints_useParameterNames()
.to_owned(),
- max_length: self.inlayHints_maxLength(source_root).to_owned(),
- closing_brace_hints_min_lines: if self
- .inlayHints_closingBraceHints_enable(source_root)
- .to_owned()
+ max_length: self.inlayHints_maxLength().to_owned(),
+ closing_brace_hints_min_lines: if self.inlayHints_closingBraceHints_enable().to_owned()
{
- Some(self.inlayHints_closingBraceHints_minLines(source_root).to_owned())
+ Some(self.inlayHints_closingBraceHints_minLines().to_owned())
} else {
None
},
@@ -1233,10 +1475,8 @@ impl Config {
resolve_label_location: client_capability_fields.contains("label.location"),
resolve_label_command: client_capability_fields.contains("label.command"),
},
- implicit_drop_hints: self.inlayHints_implicitDrops_enable(source_root).to_owned(),
- range_exclusive_hints: self
- .inlayHints_rangeExclusiveHints_enable(source_root)
- .to_owned(),
+ implicit_drop_hints: self.inlayHints_implicitDrops_enable().to_owned(),
+ range_exclusive_hints: self.inlayHints_rangeExclusiveHints_enable().to_owned(),
}
}
@@ -1260,36 +1500,32 @@ impl Config {
}
}
- pub fn join_lines(&self, source_root: Option<SourceRootId>) -> JoinLinesConfig {
+ pub fn join_lines(&self) -> JoinLinesConfig {
JoinLinesConfig {
- join_else_if: self.joinLines_joinElseIf(source_root).to_owned(),
- remove_trailing_comma: self.joinLines_removeTrailingComma(source_root).to_owned(),
- unwrap_trivial_blocks: self.joinLines_unwrapTrivialBlock(source_root).to_owned(),
- join_assignments: self.joinLines_joinAssignments(source_root).to_owned(),
+ join_else_if: self.joinLines_joinElseIf().to_owned(),
+ remove_trailing_comma: self.joinLines_removeTrailingComma().to_owned(),
+ unwrap_trivial_blocks: self.joinLines_unwrapTrivialBlock().to_owned(),
+ join_assignments: self.joinLines_joinAssignments().to_owned(),
}
}
- pub fn highlighting_non_standard_tokens(&self, source_root: Option<SourceRootId>) -> bool {
- self.semanticHighlighting_nonStandardTokens(source_root).to_owned()
+ pub fn highlighting_non_standard_tokens(&self) -> bool {
+ self.semanticHighlighting_nonStandardTokens().to_owned()
}
- pub fn highlighting_config(&self, source_root: Option<SourceRootId>) -> HighlightConfig {
+ pub fn highlighting_config(&self) -> HighlightConfig {
HighlightConfig {
- strings: self.semanticHighlighting_strings_enable(source_root).to_owned(),
- punctuation: self.semanticHighlighting_punctuation_enable(source_root).to_owned(),
+ strings: self.semanticHighlighting_strings_enable().to_owned(),
+ punctuation: self.semanticHighlighting_punctuation_enable().to_owned(),
specialize_punctuation: self
- .semanticHighlighting_punctuation_specialization_enable(source_root)
- .to_owned(),
- macro_bang: self
- .semanticHighlighting_punctuation_separate_macro_bang(source_root)
+ .semanticHighlighting_punctuation_specialization_enable()
.to_owned(),
- operator: self.semanticHighlighting_operator_enable(source_root).to_owned(),
+ macro_bang: self.semanticHighlighting_punctuation_separate_macro_bang().to_owned(),
+ operator: self.semanticHighlighting_operator_enable().to_owned(),
specialize_operator: self
- .semanticHighlighting_operator_specialization_enable(source_root)
- .to_owned(),
- inject_doc_comment: self
- .semanticHighlighting_doc_comment_inject_enable(source_root)
+ .semanticHighlighting_operator_specialization_enable()
.to_owned(),
+ inject_doc_comment: self.semanticHighlighting_doc_comment_inject_enable().to_owned(),
syntactic_name_ref_highlighting: false,
}
}
@@ -2016,7 +2252,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 +2347,7 @@ enum ImportGranularityDef {
#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
-enum CallableCompletionDef {
+pub(crate) enum CallableCompletionDef {
FillArguments,
AddParentheses,
None,
@@ -2318,54 +2554,81 @@ 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 {
+ 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.$field.as_ref() {
+ return value;
+ }
+ }
}
- if let Some(v) = self.user_config.local.$field.as_ref() {
+ if let Some((root_path_ratoml, _)) = self.root_ratoml.as_ref() {
+ if let Some(v) = root_path_ratoml.local.$field.as_ref() {
+ return &v;
+ }
+ }
+
+ if let Some(v) = self.client_config.0.local.$field.as_ref() {
return &v;
}
+ if let Some((user_config, _)) = self.user_config.as_ref() {
+ if let Some(v) = user_config.local.$field.as_ref() {
+ return &v;
+ }
+ }
+
&self.default_config.local.$field
}
)*
}
};
(global, $(
- $(#[doc=$doc:literal])*
- $vis:vis $field:ident : $ty:ty = $default:expr,
- )*
- ) => {
+ $(#[doc=$doc:literal])*
+ $vis:vis $field:ident : $ty:ty = $default:expr,
+ )*
+ ) => {
impl Config {
$(
$($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.global.$field.as_ref() {
+ return &v;
+ }
}
- if let Some(v) = self.user_config.global.$field.as_ref() {
+ if let Some(v) = self.client_config.0.global.$field.as_ref() {
return &v;
}
+ if let Some((user_config, _)) = self.user_config.as_ref() {
+ if let Some(v) = user_config.global.$field.as_ref() {
+ return &v;
+ }
+ }
+
&self.default_config.global.$field
}
)*
}
};
(client, $(
- $(#[doc=$doc:literal])*
- $vis:vis $field:ident : $ty:ty = $default:expr,
- )*
+ $(#[doc=$doc:literal])*
+ $vis:vis $field:ident : $ty:ty = $default:expr,
+ )*
) => {
impl Config {
$(
$($doc)*
#[allow(non_snake_case)]
$vis fn $field(&self) -> &$ty {
- if let Some(v) = self.client_config.global.$field.as_ref() {
+ if let Some(v) = self.client_config.0.client.$field.as_ref() {
return &v;
}
@@ -2387,7 +2650,7 @@ macro_rules! _config_data {
}) => {
/// Default config values for this grouping.
#[allow(non_snake_case)]
- #[derive(Debug, Clone, Serialize)]
+ #[derive(Debug, Clone )]
struct $name { $($field: $ty,)* }
impl_for_config_data!{
@@ -2425,26 +2688,10 @@ macro_rules! _config_data {
}
}
- #[allow(unused)]
- impl $name {
- /// Applies overrides from some more local config blob, to self.
- fn apply_input(&mut self, input: $input) {
- $(
- if let Some(value) = input.$field {
- self.$field = value;
- }
- )*
- }
-
- fn clone_with_overrides(&self, input: $input) -> Self {
- Self {$(
- $field: input.$field.unwrap_or_else(|| self.$field.clone()),
- )*}
- }
- }
-
#[allow(unused, clippy::ptr_arg)]
impl $input {
+ const FIELDS: &'static [&'static str] = &[$(stringify!($field)),*];
+
fn from_json(json: &mut serde_json::Value, error_sink: &mut Vec<(String, serde_json::Error)>) -> Self {
Self {$(
$field: get_field(
@@ -2456,7 +2703,7 @@ macro_rules! _config_data {
)*}
}
- fn from_toml(toml: &mut toml::Table, error_sink: &mut Vec<(String, toml::de::Error)>) -> Self {
+ fn from_toml(toml: &toml::Table, error_sink: &mut Vec<(String, toml::de::Error)>) -> Self {
Self {$(
$field: get_field_toml::<$ty>(
toml,
@@ -2483,8 +2730,7 @@ macro_rules! _config_data {
mod $modname {
#[test]
fn fields_are_sorted() {
- let field_names: &'static [&'static str] = &[$(stringify!($field)),*];
- field_names.windows(2).for_each(|w| assert!(w[0] <= w[1], "{} <= {} does not hold", w[0], w[1]));
+ super::$input::FIELDS.windows(2).for_each(|w| assert!(w[0] <= w[1], "{} <= {} does not hold", w[0], w[1]));
}
}
};
@@ -2495,18 +2741,16 @@ use _config_data as config_data;
struct DefaultConfigData {
global: GlobalDefaultConfigData,
local: LocalDefaultConfigData,
- #[allow(dead_code)]
client: ClientDefaultConfigData,
}
/// 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,
}
@@ -2527,7 +2771,6 @@ impl FullConfigInput {
GlobalConfigInput::schema_fields(&mut fields);
LocalConfigInput::schema_fields(&mut fields);
ClientConfigInput::schema_fields(&mut fields);
- // HACK: sort the fields, so the diffs on the generated docs/schema are smaller
fields.sort_by_key(|&(x, ..)| x);
fields
}
@@ -2545,63 +2788,57 @@ 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,
}
impl GlobalLocalConfigInput {
- #[allow(dead_code)]
+ const FIELDS: &'static [&'static [&'static str]] =
+ &[GlobalConfigInput::FIELDS, LocalConfigInput::FIELDS];
fn from_toml(
- mut toml: toml::Table,
+ toml: toml::Table,
error_sink: &mut Vec<(String, toml::de::Error)>,
) -> GlobalLocalConfigInput {
GlobalLocalConfigInput {
- global: GlobalConfigInput::from_toml(&mut toml, error_sink),
- local: LocalConfigInput::from_toml(&mut toml, error_sink),
+ global: GlobalConfigInput::from_toml(&toml, error_sink),
+ local: LocalConfigInput::from_toml(&toml, error_sink),
}
}
}
-fn get_field_toml<T: DeserializeOwned>(
- val: &toml::Table,
- error_sink: &mut Vec<(String, toml::de::Error)>,
+fn get_field<T: DeserializeOwned>(
+ json: &mut serde_json::Value,
+ error_sink: &mut Vec<(String, serde_json::Error)>,
field: &'static str,
alias: Option<&'static str>,
) -> Option<T> {
+ // XXX: check alias first, to work around the VS Code where it pre-fills the
+ // defaults instead of sending an empty object.
alias
.into_iter()
.chain(iter::once(field))
.filter_map(move |field| {
- let subkeys = field.split('_');
- let mut v = val;
- for subkey in subkeys {
- if let Some(val) = v.get(subkey) {
- if let Some(map) = val.as_table() {
- v = map;
- } else {
- return Some(toml::Value::try_into(val.clone()).map_err(|e| (e, v)));
- }
- } else {
- return None;
- }
- }
- None
+ let mut pointer = field.replace('_', "/");
+ pointer.insert(0, '/');
+ json.pointer_mut(&pointer)
+ .map(|it| serde_json::from_value(it.take()).map_err(|e| (e, pointer)))
})
.find(Result::is_ok)
.and_then(|res| match res {
Ok(it) => Some(it),
Err((e, pointer)) => {
- error_sink.push((pointer.to_string(), e));
+ tracing::warn!("Failed to deserialize config field at {}: {:?}", pointer, e);
+ error_sink.push((pointer, e));
None
}
})
}
-fn get_field<T: DeserializeOwned>(
- json: &mut serde_json::Value,
- error_sink: &mut Vec<(String, serde_json::Error)>,
+fn get_field_toml<T: DeserializeOwned>(
+ toml: &toml::Table,
+ error_sink: &mut Vec<(String, toml::de::Error)>,
field: &'static str,
alias: Option<&'static str>,
) -> Option<T> {
@@ -2613,8 +2850,8 @@ fn get_field<T: DeserializeOwned>(
.filter_map(move |field| {
let mut pointer = field.replace('_', "/");
pointer.insert(0, '/');
- json.pointer_mut(&pointer)
- .map(|it| serde_json::from_value(it.take()).map_err(|e| (e, pointer)))
+ toml_pointer(toml, &pointer)
+ .map(|it| <_>::deserialize(it.clone()).map_err(|e| (e, pointer)))
})
.find(Result::is_ok)
.and_then(|res| match res {
@@ -2627,6 +2864,32 @@ fn get_field<T: DeserializeOwned>(
})
}
+fn toml_pointer<'a>(toml: &'a toml::Table, pointer: &str) -> Option<&'a toml::Value> {
+ fn parse_index(s: &str) -> Option<usize> {
+ if s.starts_with('+') || (s.starts_with('0') && s.len() != 1) {
+ return None;
+ }
+ s.parse().ok()
+ }
+
+ if pointer.is_empty() {
+ return None;
+ }
+ if !pointer.starts_with('/') {
+ return None;
+ }
+ let mut parts = pointer.split('/').skip(1);
+ let first = parts.next()?;
+ let init = toml.get(first)?;
+ parts.map(|x| x.replace("~1", "/").replace("~0", "~")).try_fold(init, |target, token| {
+ match target {
+ toml::Value::Table(table) => table.get(&token),
+ toml::Value::Array(list) => parse_index(&token).and_then(move |x| list.get(x)),
+ _ => None,
+ }
+ })
+}
+
type SchemaField = (&'static str, &'static str, &'static [&'static str], String);
fn schema(fields: &[SchemaField]) -> serde_json::Value {
@@ -3000,6 +3263,34 @@ fn field_props(field: &str, ty: &str, doc: &[&str], default: &str) -> serde_json
map.into()
}
+fn validate_toml_table(
+ known_ptrs: &[&[&'static str]],
+ toml: &toml::Table,
+ ptr: &mut String,
+ error_sink: &mut Vec<(String, toml::de::Error)>,
+) {
+ let verify = |ptr: &String| known_ptrs.iter().any(|ptrs| ptrs.contains(&ptr.as_str()));
+
+ let l = ptr.len();
+ for (k, v) in toml {
+ if !ptr.is_empty() {
+ ptr.push('_');
+ }
+ ptr.push_str(k);
+
+ match v {
+ // This is a table config, any entry in it is therefor valid
+ toml::Value::Table(_) if verify(ptr) => (),
+ toml::Value::Table(table) => validate_toml_table(known_ptrs, table, ptr, error_sink),
+ _ if !verify(ptr) => error_sink
+ .push((ptr.replace('_', "/"), toml::de::Error::custom("unexpected field"))),
+ _ => (),
+ }
+
+ ptr.truncate(l);
+ }
+}
+
#[cfg(test)]
fn manual(fields: &[SchemaField]) -> String {
fields.iter().fold(String::new(), |mut acc, (field, _ty, doc, default)| {
@@ -3113,12 +3404,16 @@ 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,
+ }}));
+
+ (config, _, _) = config.apply_change(change);
assert_eq!(config.proc_macro_srv(), None);
}
@@ -3129,12 +3424,15 @@ 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(),
+ }}));
+
+ (config, _, _) = config.apply_change(change);
assert_eq!(config.proc_macro_srv(), Some(AbsPathBuf::try_from(project_root()).unwrap()));
}
@@ -3145,12 +3443,18 @@ 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"
+ }}));
+
+ (config, _, _) = config.apply_change(change);
+
assert_eq!(
config.proc_macro_srv(),
Some(AbsPathBuf::try_from(project_root().join("./server")).unwrap())
@@ -3164,12 +3468,16 @@ 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 }
+ }));
+
+ (config, _, _) = config.apply_change(change);
assert_eq!(config.cargo_targetDir(), &None);
assert!(
matches!(config.flycheck(), FlycheckConfig::CargoCommand { options, .. } if options.target_dir.is_none())
@@ -3183,12 +3491,16 @@ 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 }
+ }));
+
+ (config, _, _) = config.apply_change(change);
+
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")))
@@ -3202,12 +3514,16 @@ 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" }
+ }));
+
+ (config, _, _) = config.apply_change(change);
+
assert_eq!(
config.cargo_targetDir(),
&Some(TargetDirectory::Directory(Utf8PathBuf::from("other_folder")))
@@ -3216,4 +3532,95 @@ mod tests {
matches!(config.flycheck(), FlycheckConfig::CargoCommand { options, .. } if options.target_dir == Some(Utf8PathBuf::from("other_folder")))
);
}
+
+ #[test]
+ fn toml_unknown_key() {
+ let config = Config::new(
+ AbsPathBuf::try_from(project_root()).unwrap(),
+ Default::default(),
+ vec![],
+ None,
+ None,
+ );
+
+ let mut change = ConfigChange::default();
+
+ change.change_root_ratoml(Some(
+ toml::toml! {
+ [cargo.cfgs]
+ these = "these"
+ should = "should"
+ be = "be"
+ valid = "valid"
+
+ [invalid.config]
+ err = "error"
+
+ [cargo]
+ target = "ok"
+
+ // FIXME: This should be an error
+ [cargo.sysroot]
+ non-table = "expected"
+ }
+ .to_string()
+ .into(),
+ ));
+
+ let (config, e, _) = config.apply_change(change);
+ expect_test::expect![[r#"
+ ConfigErrors(
+ [
+ Toml {
+ config_key: "invalid/config/err",
+ error: Error {
+ inner: Error {
+ inner: TomlError {
+ message: "unexpected field",
+ raw: None,
+ keys: [],
+ span: None,
+ },
+ },
+ },
+ },
+ ],
+ )
+ "#]]
+ .assert_debug_eq(&e);
+ let mut change = ConfigChange::default();
+
+ change.change_user_config(Some(
+ toml::toml! {
+ [cargo.cfgs]
+ these = "these"
+ should = "should"
+ be = "be"
+ valid = "valid"
+ }
+ .to_string()
+ .into(),
+ ));
+ let (_, e, _) = config.apply_change(change);
+ expect_test::expect![[r#"
+ ConfigErrors(
+ [
+ Toml {
+ config_key: "invalid/config/err",
+ error: Error {
+ inner: Error {
+ inner: TomlError {
+ message: "unexpected field",
+ raw: None,
+ keys: [],
+ span: None,
+ },
+ },
+ },
+ },
+ ],
+ )
+ "#]]
+ .assert_debug_eq(&e);
+ }
}
diff --git a/crates/rust-analyzer/src/diagnostics.rs b/crates/rust-analyzer/src/diagnostics.rs
index 9f1893ff0e..6798e058db 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..59431d7d42 100644
--- a/crates/rust-analyzer/src/global_state.rs
+++ b/crates/rust-analyzer/src/global_state.rs
@@ -3,13 +3,13 @@
//!
//! Each tick provides an immutable snapshot of the state as `WorldSnapshot`.
-use std::time::Instant;
+use std::{ops::Not as _, time::Instant};
use crossbeam_channel::{unbounded, Receiver, Sender};
use flycheck::FlycheckHandle;
use hir::ChangeWithProcMacros;
use ide::{Analysis, AnalysisHost, Cancellable, FileId, SourceRootId};
-use ide_db::base_db::{CrateId, ProcMacroPaths};
+use ide_db::base_db::{CrateId, ProcMacroPaths, SourceDatabaseExt};
use load_cargo::SourceRootConfig;
use lsp_types::{SemanticTokens, Url};
use nohash_hasher::IntMap;
@@ -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, ChangeKind, Vfs};
use crate::{
- config::{Config, ConfigError},
+ config::{Config, ConfigChange, ConfigErrors},
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,
@@ -65,13 +68,13 @@ pub(crate) struct GlobalState {
pub(crate) fmt_pool: Handle<TaskPool<Task>, Receiver<Task>>,
pub(crate) config: Arc<Config>,
- pub(crate) config_errors: Option<ConfigError>,
+ pub(crate) config_errors: Option<ConfigErrors>,
pub(crate) analysis_host: AnalysisHost,
pub(crate) diagnostics: DiagnosticCollection,
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,14 @@ 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, (ChangeKind, vfs::VfsPath)> =
+ FxHashMap::default();
+
let (change, modified_rust_files, workspace_structure_change) = {
let mut change = ChangeWithProcMacros::new();
let mut guard = self.vfs.write();
@@ -273,6 +284,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, (file.kind(), vfs_path.clone()));
+ }
+
if let Some(path) = vfs_path.as_path() {
has_structure_changes |= file.is_created_or_deleted();
@@ -310,12 +326,15 @@ impl GlobalState {
bytes.push((file.file_id, text));
}
let (vfs, line_endings_map) = &mut *RwLockUpgradableReadGuard::upgrade(guard);
- bytes.into_iter().for_each(|(file_id, text)| match text {
- None => change.change_file(file_id, None),
- Some((text, line_endings)) => {
- line_endings_map.insert(file_id, line_endings);
- change.change_file(file_id, Some(text));
- }
+ bytes.into_iter().for_each(|(file_id, text)| {
+ let text = match text {
+ None => None,
+ Some((text, line_endings)) => {
+ line_endings_map.insert(file_id, line_endings);
+ Some(text)
+ }
+ };
+ change.change_file(file_id, text);
});
if has_structure_changes {
let roots = self.source_root_config.partition(vfs);
@@ -326,6 +345,63 @@ impl GlobalState {
let _p = span!(Level::INFO, "GlobalState::process_changes/apply_change").entered();
self.analysis_host.apply_change(change);
+ if !modified_ratoml_files.is_empty()
+ || !self.config.same_source_root_parent_map(&self.local_roots_parent_map)
+ {
+ let config_change = {
+ let user_config_path = self.config.user_config_path();
+ let root_ratoml_path = self.config.root_ratoml_path();
+ let mut change = ConfigChange::default();
+ let db = self.analysis_host.raw_database();
+
+ for (file_id, (_change_kind, vfs_path)) in modified_ratoml_files {
+ if vfs_path == *user_config_path {
+ change.change_user_config(Some(db.file_text(file_id)));
+ continue;
+ }
+
+ if vfs_path == *root_ratoml_path {
+ change.change_root_ratoml(Some(db.file_text(file_id)));
+ continue;
+ }
+
+ // If change has been made to a ratoml file that
+ // belongs to a non-local source root, we will ignore it.
+ // As it doesn't make sense a users to use external config files.
+ let sr_id = db.file_source_root(file_id);
+ let sr = db.source_root(sr_id);
+ if !sr.is_library {
+ if let Some((old_path, old_text)) = change.change_ratoml(
+ sr_id,
+ vfs_path.clone(),
+ Some(db.file_text(file_id)),
+ ) {
+ // 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(sr_id, old_path, old_text);
+ }
+ }
+ } else {
+ // Mapping to a SourceRoot should always end up in `Ok`
+ span!(Level::ERROR, "Mapping to SourceRootId failed.");
+ }
+ }
+ change.change_source_root_parent_map(self.local_roots_parent_map.clone());
+ change
+ };
+
+ let (config, e, should_update) = self.config.apply_change(config_change);
+ self.config_errors = e.is_empty().not().then_some(e);
+
+ if should_update {
+ self.update_configuration(config);
+ } else {
+ // No global or client level config was changed. So we can just naively replace config.
+ self.config = Arc::new(config);
+ }
+ }
{
if !matches!(&workspace_structure_change, Some((.., true))) {
diff --git a/crates/rust-analyzer/src/handlers/notification.rs b/crates/rust-analyzer/src/handlers/notification.rs
index 2d5fc9bfac..2dbc297ea6 100644
--- a/crates/rust-analyzer/src/handlers/notification.rs
+++ b/crates/rust-analyzer/src/handlers/notification.rs
@@ -1,7 +1,7 @@
//! This module is responsible for implementing handlers for Language Server
//! Protocol. This module specifically handles notifications.
-use std::ops::Deref;
+use std::ops::{Deref, Not as _};
use itertools::Itertools;
use lsp_types::{
@@ -13,7 +13,7 @@ use triomphe::Arc;
use vfs::{AbsPathBuf, ChangeKind, VfsPath};
use crate::{
- config::Config,
+ config::{Config, ConfigChange},
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,14 @@ 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 config = Config::clone(&*this.config);
+ let mut change = ConfigChange::default();
+ change.change_client_config(json.take());
+
+ let (config, e, _) = config.apply_change(change);
+ this.config_errors = e.is_empty().not().then_some(e);
+
+ // Client config changes neccesitates .update_config method to be called.
this.update_configuration(config);
}
}
diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs
index 50d405c952..0789dd6462 100644
--- a/crates/rust-analyzer/src/handlers/request.rs
+++ b/crates/rust-analyzer/src/handlers/request.rs
@@ -42,6 +42,7 @@ use crate::{
hack_recover_crate_name,
line_index::LineEndings,
lsp::{
+ ext::InternalTestingFetchConfigParams,
from_proto, to_proto,
utils::{all_edits_are_disjoint, invalid_params_error},
LspError,
@@ -367,8 +368,7 @@ pub(crate) fn handle_join_lines(
let _p = tracing::info_span!("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 config = snap.config.join_lines(Some(source_root));
+ let config = snap.config.join_lines();
let line_index = snap.file_line_index(file_id)?;
let mut res = TextEdit::default();
@@ -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::info_span!("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,13 +1511,12 @@ 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 range = TextRange::new(
range.start().min(line_index.index.len()),
range.end().min(line_index.index.len()),
);
- let inlay_hints_config = snap.config.inlay_hints(Some(source_root));
+ let inlay_hints_config = snap.config.inlay_hints();
Ok(Some(
snap.analysis
.inlay_hints(&inlay_hints_config, file_id, Some(range))?
@@ -1553,9 +1552,8 @@ 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 mut forced_resolve_inlay_hints_config = snap.config.inlay_hints(Some(source_root));
+ let mut forced_resolve_inlay_hints_config = snap.config.inlay_hints();
forced_resolve_inlay_hints_config.fields_to_resolve = InlayFieldsToResolve::empty();
let resolve_hints = snap.analysis.inlay_hints_resolve(
&forced_resolve_inlay_hints_config,
@@ -1687,9 +1685,8 @@ 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 mut highlight_config = snap.config.highlighting_config(Some(source_root));
+ let mut highlight_config = snap.config.highlighting_config();
// Avoid flashing a bunch of unresolved references when the proc-macro servers haven't been spawned yet.
highlight_config.syntactic_name_ref_highlighting =
snap.workspaces.is_empty() || !snap.proc_macros_loaded;
@@ -1700,7 +1697,7 @@ pub(crate) fn handle_semantic_tokens_full(
&line_index,
highlights,
snap.config.semantics_tokens_augments_syntax_tokens(),
- snap.config.highlighting_non_standard_tokens(Some(source_root)),
+ snap.config.highlighting_non_standard_tokens(),
);
// Unconditionally cache the tokens
@@ -1718,9 +1715,8 @@ 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 mut highlight_config = snap.config.highlighting_config(Some(source_root));
+ let mut highlight_config = snap.config.highlighting_config();
// Avoid flashing a bunch of unresolved references when the proc-macro servers haven't been spawned yet.
highlight_config.syntactic_name_ref_highlighting =
snap.workspaces.is_empty() || !snap.proc_macros_loaded;
@@ -1731,7 +1727,7 @@ pub(crate) fn handle_semantic_tokens_full_delta(
&line_index,
highlights,
snap.config.semantics_tokens_augments_syntax_tokens(),
- snap.config.highlighting_non_standard_tokens(Some(source_root)),
+ snap.config.highlighting_non_standard_tokens(),
);
let cached_tokens = snap.semantic_tokens_cache.lock().remove(&params.text_document.uri);
@@ -1762,9 +1758,8 @@ 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 mut highlight_config = snap.config.highlighting_config(Some(source_root));
+ let mut highlight_config = snap.config.highlighting_config();
// Avoid flashing a bunch of unresolved references when the proc-macro servers haven't been spawned yet.
highlight_config.syntactic_name_ref_highlighting =
snap.workspaces.is_empty() || !snap.proc_macros_loaded;
@@ -1775,7 +1770,7 @@ pub(crate) fn handle_semantic_tokens_range(
&line_index,
highlights,
snap.config.semantics_tokens_augments_syntax_tokens(),
- snap.config.highlighting_non_standard_tokens(Some(source_root)),
+ snap.config.highlighting_non_standard_tokens(),
);
Ok(Some(semantic_tokens.into()))
}
@@ -1991,8 +1986,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;
@@ -2237,6 +2232,30 @@ pub(crate) fn fetch_dependency_list(
Ok(FetchDependencyListResult { crates: crate_infos })
}
+pub(crate) fn internal_testing_fetch_config(
+ state: GlobalStateSnapshot,
+ params: InternalTestingFetchConfigParams,
+) -> anyhow::Result<serde_json::Value> {
+ let source_root = params
+ .text_document
+ .map(|it| {
+ state
+ .analysis
+ .source_root_id(from_proto::file_id(&state, &it.uri)?)
+ .map_err(anyhow::Error::from)
+ })
+ .transpose()?;
+ serde_json::to_value(match &*params.config {
+ "local" => state.config.assist(source_root).assist_emit_must_use,
+ "global" => matches!(
+ state.config.rustfmt(),
+ RustfmtConfig::Rustfmt { enable_range_formatting: true, .. }
+ ),
+ _ => return Err(anyhow::anyhow!("Unknown test config key: {}", params.config)),
+ })
+ .map_err(Into::into)
+}
+
/// Searches for the directory of a Rust crate given this crate's root file path.
///
/// # Arguments
diff --git a/crates/rust-analyzer/src/lib.rs b/crates/rust-analyzer/src/lib.rs
index 175ffa622f..b3c11d0156 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;
+mod global_state;
pub mod lsp;
use self::lsp::ext as lsp_ext;
diff --git a/crates/rust-analyzer/src/lsp/ext.rs b/crates/rust-analyzer/src/lsp/ext.rs
index aa75633ac3..4da9054d13 100644
--- a/crates/rust-analyzer/src/lsp/ext.rs
+++ b/crates/rust-analyzer/src/lsp/ext.rs
@@ -17,6 +17,20 @@ use serde::{Deserialize, Serialize};
use crate::line_index::PositionEncoding;
+pub enum InternalTestingFetchConfig {}
+
+impl Request for InternalTestingFetchConfig {
+ type Params = InternalTestingFetchConfigParams;
+ type Result = serde_json::Value;
+ const METHOD: &'static str = "rust-analyzer-internal/internalTestingFetchConfig";
+}
+
+#[derive(Deserialize, Serialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct InternalTestingFetchConfigParams {
+ pub text_document: Option<TextDocumentIdentifier>,
+ pub config: String,
+}
pub enum AnalyzerStatus {}
impl Request for AnalyzerStatus {
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs
index 864dc9623f..9b19e58eaa 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()),
+ },
]),
},
};
@@ -474,6 +479,7 @@ impl GlobalState {
fn update_diagnostics(&mut self) {
let db = self.analysis_host.raw_database();
+ // spawn a task per subscription?
let subscriptions = {
let vfs = &self.vfs.read().0;
self.mem_docs
@@ -971,6 +977,8 @@ impl GlobalState {
.on::<NO_RETRY, lsp_ext::ExternalDocs>(handlers::handle_open_docs)
.on::<NO_RETRY, lsp_ext::OpenCargoToml>(handlers::handle_open_cargo_toml)
.on::<NO_RETRY, lsp_ext::MoveItem>(handlers::handle_move_item)
+ //
+ .on::<NO_RETRY, lsp_ext::InternalTestingFetchConfig>(handlers::internal_testing_fetch_config)
.finish();
}
diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs
index c07adb3c5a..9d8f2b5fcc 100644
--- a/crates/rust-analyzer/src/reload.rs
+++ b/crates/rust-analyzer/src/reload.rs
@@ -24,6 +24,7 @@ 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};
@@ -442,40 +443,59 @@ 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(|base| {
+ [
+ format!("{base}/**/*.rs"),
+ format!("{base}/**/Cargo.{{toml,lock}}"),
+ format!("{base}/**/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 };
@@ -547,7 +567,7 @@ 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());
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..f886df60e6 100644
--- a/crates/rust-analyzer/tests/slow-tests/main.rs
+++ b/crates/rust-analyzer/tests/slow-tests/main.rs
@@ -11,6 +11,7 @@
#![warn(rust_2018_idioms, unused_lifetimes)]
#![allow(clippy::disallowed_types)]
+mod ratoml;
#[cfg(not(feature = "in-rust-tree"))]
mod sourcegen;
mod support;
@@ -30,15 +31,15 @@ use lsp_types::{
InlayHint, InlayHintLabel, InlayHintParams, PartialResultParams, Position, Range,
RenameFilesParams, TextDocumentItem, TextDocumentPositionParams, WorkDoneProgressParams,
};
+
use rust_analyzer::lsp::ext::{OnEnter, Runnables, RunnablesParams, UnindexedProject};
use serde_json::json;
use stdx::format_to_acc;
+
use test_utils::skip_slow_tests;
+use testdir::TestDir;
-use crate::{
- support::{project, Project},
- testdir::TestDir,
-};
+use crate::support::{project, Project};
#[test]
fn completes_items_from_standard_library() {
diff --git a/crates/rust-analyzer/tests/slow-tests/ratoml.rs b/crates/rust-analyzer/tests/slow-tests/ratoml.rs
new file mode 100644
index 0000000000..218a9a32ad
--- /dev/null
+++ b/crates/rust-analyzer/tests/slow-tests/ratoml.rs
@@ -0,0 +1,947 @@
+use crate::support::{Project, Server};
+use crate::testdir::TestDir;
+use lsp_types::{
+ notification::{DidChangeTextDocument, DidOpenTextDocument, DidSaveTextDocument},
+ DidChangeTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams,
+ TextDocumentContentChangeEvent, TextDocumentIdentifier, TextDocumentItem, Url,
+ VersionedTextDocumentIdentifier,
+};
+use paths::Utf8PathBuf;
+
+use rust_analyzer::lsp::ext::{InternalTestingFetchConfig, InternalTestingFetchConfigParams};
+use serde_json::json;
+
+enum QueryType {
+ Local,
+ /// 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.
+ Global,
+}
+
+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 GLOBAL_TRAIT_ASSOC_ITEMS_ZERO: &'static str = r#"hover.show.traitAssocItems = 0"#;
+
+ fn new(
+ fixtures: Vec<&str>,
+ roots: Vec<&str>,
+ client_config: Option<serde_json::Value>,
+ ) -> Self {
+ let tmp_dir = TestDir::new();
+ let tmp_path = tmp_dir.path().to_owned();
+
+ let full_fixture = fixtures.join("\n");
+
+ let user_cnf_dir = TestDir::new();
+ let user_config_dir = user_cnf_dir.path().to_owned();
+
+ let mut project =
+ Project::with_fixture(&full_fixture).tmp_dir(tmp_dir).user_config_dir(user_cnf_dir);
+
+ for root in roots {
+ project = project.root(root);
+ }
+
+ if let Some(client_config) = client_config {
+ project = project.with_config(client_config);
+ }
+
+ let server = project.server().wait_until_workspace_is_loaded();
+
+ let mut case = Self { urls: vec![], server, tmp_path, user_config_dir };
+ let urls = fixtures.iter().map(|fixture| case.fixture_path(fixture)).collect::<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 {
+ let config = match query {
+ QueryType::Local => "local".to_owned(),
+ QueryType::Global => "global".to_owned(),
+ };
+ let res = self.server.send_request::<InternalTestingFetchConfig>(
+ InternalTestingFetchConfigParams {
+ text_document: Some(TextDocumentIdentifier {
+ uri: self.urls[source_file_idx].clone(),
+ }),
+ config,
+ },
+ );
+ res.as_bool().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::Local, 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]
+#[ignore = "the user config is currently not being watched on startup, fix this"]
+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::Local, 2));
+}
+
+#[test]
+#[ignore = "the user config is currently not being watched on startup, fix this"]
+fn ratoml_create_user_config() {
+ let mut server = RatomlTest::new(
+ vec![
+ r#"
+//- /p1/Cargo.toml
+[package]
+name = "p1"
+version = "0.1.0"
+edition = "2021"
+"#,
+ r#"
+//- /p1/src/lib.rs
+enum Value {
+ Number(i32),
+ Text(String),
+}"#,
+ ],
+ vec!["p1"],
+ None,
+ );
+
+ assert!(!server.query(QueryType::Local, 1));
+ server.create(
+ "//- /$$CONFIG_DIR$$/rust-analyzer/rust-analyzer.toml",
+ RatomlTest::EMIT_MUST_USE.to_owned(),
+ );
+ assert!(server.query(QueryType::Local, 1));
+}
+
+#[test]
+#[ignore = "the user config is currently not being watched on startup, fix this"]
+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::Local, 1));
+ server.edit(2, String::new());
+ assert!(!server.query(QueryType::Local, 1));
+}
+
+#[test]
+#[ignore = "the user config is currently not being watched on startup, fix this"]
+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::Local, 1));
+ server.delete(2);
+ assert!(!server.query(QueryType::Local, 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::Local, 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::Local, 3));
+ server.edit(1, "assist.emitMustUse = true".to_owned());
+ assert!(server.query(QueryType::Local, 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::Local, 3));
+ server.delete(1);
+ assert!(!server.query(QueryType::Local, 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::Local, 3));
+ server.create("//- /p1/p2/rust-analyzer.toml", RatomlTest::EMIT_MUST_NOT_USE.to_owned());
+ assert!(!server.query(QueryType::Local, 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::Local, 3));
+ server.delete(1);
+ assert!(!server.query(QueryType::Local, 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::Local, 3));
+ assert!(server.query(QueryType::Local, 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::Local, 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::Local, 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::Global, 2);
+}
+
+#[allow(unused)]
+// #[test]
+// FIXME: Re-enable this test when we have a global config we can check again
+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::Global, 2));
+ server.edit(1, RatomlTest::GLOBAL_TRAIT_ASSOC_ITEMS_ZERO.to_owned());
+ assert!(!server.query(QueryType::Global, 2));
+}
+
+#[allow(unused)]
+// #[test]
+// FIXME: Re-enable this test when we have a global config we can check again
+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::Global, 2));
+ server.delete(1);
+ assert!(!server.query(QueryType::Global, 2));
+}
diff --git a/crates/rust-analyzer/tests/slow-tests/support.rs b/crates/rust-analyzer/tests/slow-tests/support.rs
index cf27cc7eef..c438325532 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, ConfigErrors},
+ 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,16 @@ 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 error_sink: ConfigErrors;
+ (config, error_sink, _) = config.apply_change(change);
+ assert!(error_sink.is_empty(), "{error_sink:?}");
+
config.rediscover_workspaces();
Server::new(tmp_dir.keep(), config)
diff --git a/crates/rust-analyzer/tests/slow-tests/tidy.rs b/crates/rust-analyzer/tests/slow-tests/tidy.rs
index 4a7415b016..7dd6382cfa 100644
--- a/crates/rust-analyzer/tests/slow-tests/tidy.rs
+++ b/crates/rust-analyzer/tests/slow-tests/tidy.rs
@@ -185,27 +185,6 @@ Zlib OR Apache-2.0 OR MIT
}
fn check_test_attrs(path: &Path, text: &str) {
- let ignore_rule =
- "https://github.com/rust-lang/rust-analyzer/blob/master/docs/dev/style.md#ignore";
- let need_ignore: &[&str] = &[
- // This file.
- "slow-tests/tidy.rs",
- // Special case to run `#[ignore]` tests.
- "ide/src/runnables.rs",
- // A legit test which needs to be ignored, as it takes too long to run
- // :(
- "hir-def/src/nameres/collector.rs",
- // Long sourcegen test to generate lint completions.
- "ide-db/src/tests/sourcegen_lints.rs",
- // Obviously needs ignore.
- "ide-assists/src/handlers/toggle_ignore.rs",
- // See above.
- "ide-assists/src/tests/generated.rs",
- ];
- if text.contains("#[ignore") && !need_ignore.iter().any(|p| path.ends_with(p)) {
- panic!("\ndon't `#[ignore]` tests, see:\n\n {ignore_rule}\n\n {}\n", path.display(),)
- }
-
let panic_rule =
"https://github.com/rust-lang/rust-analyzer/blob/master/docs/dev/style.md#should_panic";
let need_panic: &[&str] = &[
diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md
index 1c91e856e7..100662f4ce 100644
--- a/docs/dev/lsp-extensions.md
+++ b/docs/dev/lsp-extensions.md
@@ -1,5 +1,5 @@
<!---
-lsp/ext.rs hash: 1babf76a3c2cef3b
+lsp/ext.rs hash: a85ec97f07c6a2e3
If you need to change the above hash to make the test pass, please check if you
need to adjust this doc as well and ping this issue: