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
This commit is contained in:
bors 2024-06-07 10:49:02 +00:00
commit 7c5d496ef8
23 changed files with 2114 additions and 556 deletions

10
Cargo.lock generated
View file

@ -328,6 +328,15 @@ dependencies = [
"dirs-sys",
]
[[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"
@ -1665,6 +1674,7 @@ dependencies = [
"anyhow",
"cfg",
"crossbeam-channel",
"dirs",
"dissimilar",
"expect-test",
"flycheck",

View file

@ -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,

View file

@ -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()
}
}

View file

@ -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 {

View file

@ -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"

View file

@ -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();
}

View file

@ -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(

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -547,6 +547,7 @@ mod tests {
ClientCapabilities::default(),
Vec::new(),
None,
None,
),
);
let snap = state.snapshot();

View file

@ -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))) {

View file

@ -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);
}
}

View file

@ -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

View file

@ -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;

View file

@ -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 {

View file

@ -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();
}

View file

@ -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);

View file

@ -13,6 +13,7 @@ use tracing_tree::HierarchicalLayer;
use crate::tracing::hprof;
#[derive(Debug)]
pub struct Config<T> {
pub writer: T,
pub filter: String,

View file

@ -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 crate::{
support::{project, Project},
testdir::TestDir,
};
use test_utils::skip_slow_tests;
use testdir::TestDir;
use crate::support::{project, Project};
#[test]
fn completes_items_from_standard_library() {

View file

@ -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));
}

View file

@ -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)

View file

@ -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] = &[

View file

@ -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: