Centralize all config

This commit is contained in:
Aleksey Kladov 2020-04-01 18:41:43 +02:00
parent 1e012eb991
commit a97e5eb85d
9 changed files with 153 additions and 330 deletions

View file

@ -22,12 +22,22 @@ use crate::conv::{map_rust_diagnostic_to_lsp, MappedRustDiagnostic};
pub use crate::conv::url_from_path_with_drive_lowercasing;
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FlycheckConfig {
CargoCommand { command: String, all_targets: bool, extra_args: Vec<String> },
CustomCommand { command: String, args: Vec<String> },
}
impl Default for FlycheckConfig {
fn default() -> Self {
FlycheckConfig::CargoCommand {
command: "check".to_string(),
all_targets: true,
extra_args: Vec::new(),
}
}
}
/// Flycheck wraps the shared state and communication machinery used for
/// running `cargo check` (or other compatible command) and providing
/// diagnostics based on the output.

View file

@ -5,7 +5,7 @@ mod args;
use lsp_server::Connection;
use rust_analyzer::{cli, from_json, show_message, Result, ServerConfig};
use rust_analyzer::{cli, from_json, Config, Result};
use crate::args::HelpPrinted;
@ -78,24 +78,18 @@ fn run_server() -> Result<()> {
.filter(|workspaces| !workspaces.is_empty())
.unwrap_or_else(|| vec![root]);
let server_config = initialize_params
.initialization_options
.and_then(|v| {
from_json::<ServerConfig>("config", v)
.map_err(|e| {
log::error!("{}", e);
show_message(lsp_types::MessageType::Error, e.to_string(), &connection.sender);
})
.ok()
})
.unwrap_or_default();
let config = {
let mut config = Config::default();
if let Some(value) = &initialize_params.initialization_options {
config.update(value);
}
if let Some(caps) = &initialize_params.capabilities.text_document {
config.update_caps(caps);
}
config
};
rust_analyzer::main_loop(
workspace_roots,
initialize_params.capabilities,
server_config,
connection,
)?;
rust_analyzer::main_loop(workspace_roots, config, connection)?;
log::info!("shutting down IO...");
io_threads.join()?;

View file

@ -7,14 +7,11 @@
//! configure the server itself, feature flags are passed into analysis, and
//! tweak things like automatic insertion of `()` in completions.
use rustc_hash::FxHashMap;
use crate::feature_flags::FeatureFlags;
use lsp_types::TextDocumentClientCapabilities;
use ra_flycheck::FlycheckConfig;
use ra_ide::{CompletionConfig, InlayHintsConfig};
use ra_project_model::CargoFeatures;
use serde::{Deserialize, Deserializer};
use serde::Deserialize;
#[derive(Debug, Clone)]
pub struct Config {
@ -61,171 +58,109 @@ impl Default for RustfmtConfig {
}
}
pub(crate) fn get_config(
config: &ServerConfig,
text_document_caps: Option<&TextDocumentClientCapabilities>,
) -> Config {
let feature_flags = get_feature_flags(config);
impl Default for Config {
fn default() -> Self {
Config {
publish_decorations: config.publish_decorations,
publish_diagnostics: feature_flags.get("lsp.diagnostics"),
publish_decorations: false,
publish_diagnostics: true,
notifications: NotificationsConfig {
workspace_loaded: feature_flags.get("notifications.workspace-loaded"),
cargo_toml_not_found: feature_flags.get("notifications.cargo-toml-not-found"),
workspace_loaded: true,
cargo_toml_not_found: true,
},
supports_location_link: text_document_caps
.and_then(|it| it.definition)
.and_then(|it| it.link_support)
.unwrap_or(false),
line_folding_only: text_document_caps
.and_then(|it| it.folding_range.as_ref())
.and_then(|it| it.line_folding_only)
.unwrap_or(false),
supports_location_link: false,
line_folding_only: false,
inlay_hints: InlayHintsConfig {
type_hints: config.inlay_hints_type,
parameter_hints: config.inlay_hints_parameter,
chaining_hints: config.inlay_hints_chaining,
max_length: config.inlay_hints_max_length,
type_hints: true,
parameter_hints: true,
chaining_hints: true,
max_length: None,
},
completion: CompletionConfig {
enable_postfix_completions: feature_flags.get("completion.enable-postfix"),
add_call_parenthesis: feature_flags.get("completion.insertion.add-call-parenthesis"),
add_call_argument_snippets: feature_flags
.get("completion.insertion.add-argument-snippets"),
enable_postfix_completions: true,
add_call_parenthesis: true,
add_call_argument_snippets: true,
},
call_info_full: feature_flags.get("call-info.full"),
check: if config.cargo_watch_enable {
Some(FlycheckConfig::CargoCommand {
command: config.cargo_watch_command.clone(),
all_targets: config.cargo_watch_all_targets,
extra_args: config.cargo_watch_args.clone(),
})
} else {
None
},
rustfmt: RustfmtConfig::Rustfmt { extra_args: config.rustfmt_args.clone() },
vscode_lldb: config.vscode_lldb,
proc_macro_srv: None, // FIXME: get this from config
lru_capacity: config.lru_capacity,
use_client_watching: config.use_client_watching,
exclude_globs: config.exclude_globs.clone(),
cargo: config.cargo_features.clone(),
with_sysroot: config.with_sysroot,
}
}
fn get_feature_flags(config: &ServerConfig) -> FeatureFlags {
let mut ff = FeatureFlags::default();
for (flag, &value) in &config.feature_flags {
if ff.set(flag.as_str(), value).is_err() {
log::error!("unknown feature flag: {:?}", flag);
}
}
log::info!("feature_flags: {:#?}", ff);
ff
}
/// Client provided initialization options
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase", default)]
pub struct ServerConfig {
/// Whether the client supports our custom highlighting publishing decorations.
/// This is different to the highlightingOn setting, which is whether the user
/// wants our custom highlighting to be used.
///
/// Defaults to `false`
#[serde(deserialize_with = "nullable_bool_false")]
pub publish_decorations: bool,
pub exclude_globs: Vec<String>,
#[serde(deserialize_with = "nullable_bool_false")]
pub use_client_watching: bool,
pub lru_capacity: Option<usize>,
#[serde(deserialize_with = "nullable_bool_true")]
pub inlay_hints_type: bool,
#[serde(deserialize_with = "nullable_bool_true")]
pub inlay_hints_parameter: bool,
#[serde(deserialize_with = "nullable_bool_true")]
pub inlay_hints_chaining: bool,
pub inlay_hints_max_length: Option<usize>,
pub cargo_watch_enable: bool,
pub cargo_watch_args: Vec<String>,
pub cargo_watch_command: String,
pub cargo_watch_all_targets: bool,
/// For internal usage to make integrated tests faster.
#[serde(deserialize_with = "nullable_bool_true")]
pub with_sysroot: bool,
/// Fine grained feature flags to disable specific features.
pub feature_flags: FxHashMap<String, bool>,
pub rustfmt_args: Vec<String>,
/// Cargo feature configurations.
pub cargo_features: CargoFeatures,
/// Enabled if the vscode_lldb extension is available.
pub vscode_lldb: bool,
}
impl Default for ServerConfig {
fn default() -> ServerConfig {
ServerConfig {
publish_decorations: false,
exclude_globs: Vec::new(),
use_client_watching: false,
lru_capacity: None,
inlay_hints_type: true,
inlay_hints_parameter: true,
inlay_hints_chaining: true,
inlay_hints_max_length: None,
cargo_watch_enable: true,
cargo_watch_args: Vec::new(),
cargo_watch_command: "check".to_string(),
cargo_watch_all_targets: true,
with_sysroot: true,
feature_flags: FxHashMap::default(),
cargo_features: Default::default(),
rustfmt_args: Vec::new(),
call_info_full: true,
rustfmt: RustfmtConfig::default(),
check: Some(FlycheckConfig::default()),
vscode_lldb: false,
proc_macro_srv: None,
lru_capacity: None,
use_client_watching: false,
exclude_globs: Vec::new(),
cargo: CargoFeatures::default(),
with_sysroot: true,
}
}
}
/// Deserializes a null value to a bool false by default
fn nullable_bool_false<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: Deserializer<'de>,
{
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or(false))
}
impl Config {
#[rustfmt::skip]
pub fn update(&mut self, value: &serde_json::Value) {
let line_folding_only = self.line_folding_only;
let supports_location_link = self.supports_location_link;
*self = Default::default();
self.line_folding_only = line_folding_only;
self.supports_location_link = supports_location_link;
/// Deserializes a null value to a bool true by default
fn nullable_bool_true<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: Deserializer<'de>,
{
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or(true))
}
set(value, "publishDecorations", &mut self.publish_decorations);
set(value, "excludeGlobs", &mut self.exclude_globs);
set(value, "useClientWatching", &mut self.use_client_watching);
set(value, "lruCapacity", &mut self.lru_capacity);
#[cfg(test)]
mod test {
use super::*;
set(value, "inlayHintsType", &mut self.inlay_hints.type_hints);
set(value, "inlayHintsParameter", &mut self.inlay_hints.parameter_hints);
set(value, "inlayHintsChaining", &mut self.inlay_hints.chaining_hints);
set(value, "inlayHintsMaxLength", &mut self.inlay_hints.max_length);
#[test]
fn deserialize_init_options_defaults() {
// check that null == default for both fields
let default = ServerConfig::default();
assert_eq!(default, serde_json::from_str(r#"{}"#).unwrap());
assert_eq!(
default,
serde_json::from_str(r#"{"publishDecorations":null, "lruCapacity":null}"#).unwrap()
);
if let Some(false) = get(value, "cargo_watch_enable") {
self.check = None
} else {
if let Some(FlycheckConfig::CargoCommand { command, extra_args, all_targets }) = &mut self.check
{
set(value, "cargoWatchArgs", extra_args);
set(value, "cargoWatchCommand", command);
set(value, "cargoWatchAllTargets", all_targets);
}
};
set(value, "withSysroot", &mut self.with_sysroot);
if let RustfmtConfig::Rustfmt { extra_args } = &mut self.rustfmt {
set(value, "rustfmtArgs", extra_args);
}
set(value, "cargoFeatures/noDefaultFeatures", &mut self.cargo.no_default_features);
set(value, "cargoFeatures/allFeatures", &mut self.cargo.all_features);
set(value, "cargoFeatures/features", &mut self.cargo.features);
set(value, "cargoFeatures/loadOutDirsFromCheck", &mut self.cargo.load_out_dirs_from_check);
set(value, "vscodeLldb", &mut self.vscode_lldb);
set(value, "featureFlags/lsp.diagnostics", &mut self.publish_diagnostics);
set(value, "featureFlags/notifications.workspace-loaded", &mut self.notifications.workspace_loaded);
set(value, "featureFlags/notifications.cargo-toml-not-found", &mut self.notifications.cargo_toml_not_found);
set(value, "featureFlags/completion.enable-postfix", &mut self.completion.enable_postfix_completions);
set(value, "featureFlags/completion.insertion.add-call-parenthesis", &mut self.completion.add_call_parenthesis);
set(value, "featureFlags/completion.insertion.add-argument-snippets", &mut self.completion.add_call_argument_snippets);
set(value, "featureFlags/call-info.full", &mut self.call_info_full);
fn get<'a, T: Deserialize<'a>>(value: &'a serde_json::Value, pointer: &str) -> Option<T> {
value.pointer(pointer).and_then(|it| T::deserialize(it).ok())
}
fn set<'a, T: Deserialize<'a>>(value: &'a serde_json::Value, pointer: &str, slot: &mut T) {
if let Some(new_value) = get(value, pointer) {
*slot = new_value
}
}
}
pub fn update_caps(&mut self, caps: &TextDocumentClientCapabilities) {
if let Some(value) = caps.definition.as_ref().and_then(|it| it.link_support) {
self.supports_location_link = value;
}
if let Some(value) = caps.folding_range.as_ref().and_then(|it| it.line_folding_only) {
self.line_folding_only = value
}
}
}

View file

@ -1,77 +0,0 @@
//! See docs for `FeatureFlags`.
use rustc_hash::FxHashMap;
// FIXME: looks like a much better design is to pass options to each call,
// rather than to have a global ambient feature flags -- that way, the clients
// can issue two successive calls with different options.
/// Feature flags hold fine-grained toggles for all *user-visible* features of
/// rust-analyzer.
///
/// The exists such that users are able to disable any annoying feature (and,
/// with many users and many features, some features are bound to be annoying
/// for some users)
///
/// Note that we purposefully use run-time checked strings, and not something
/// checked at compile time, to keep things simple and flexible.
///
/// Also note that, at the moment, `FeatureFlags` also store features for
/// `rust-analyzer`. This should be benign layering violation.
#[derive(Debug)]
pub struct FeatureFlags {
flags: FxHashMap<String, bool>,
}
impl FeatureFlags {
fn new(flags: &[(&str, bool)]) -> FeatureFlags {
let flags = flags
.iter()
.map(|&(name, value)| {
check_flag_name(name);
(name.to_string(), value)
})
.collect();
FeatureFlags { flags }
}
pub fn set(&mut self, flag: &str, value: bool) -> Result<(), ()> {
match self.flags.get_mut(flag) {
None => Err(()),
Some(slot) => {
*slot = value;
Ok(())
}
}
}
pub fn get(&self, flag: &str) -> bool {
match self.flags.get(flag) {
None => panic!("unknown flag: {:?}", flag),
Some(value) => *value,
}
}
}
impl Default for FeatureFlags {
fn default() -> FeatureFlags {
FeatureFlags::new(&[
("lsp.diagnostics", true),
("completion.insertion.add-call-parenthesis", true),
("completion.insertion.add-argument-snippets", true),
("completion.enable-postfix", true),
("call-info.full", true),
("notifications.workspace-loaded", true),
("notifications.cargo-toml-not-found", true),
])
}
}
fn check_flag_name(flag: &str) {
for c in flag.bytes() {
match c {
b'a'..=b'z' | b'-' | b'.' => (),
_ => panic!("flag name does not match conventions: {:?}", flag),
}
}
}

View file

@ -37,14 +37,13 @@ mod config;
mod world;
mod diagnostics;
mod semantic_tokens;
mod feature_flags;
use serde::de::DeserializeOwned;
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
pub use crate::{
config::Config,
caps::server_capabilities,
config::ServerConfig,
main_loop::LspError,
main_loop::{main_loop, show_message},
};

View file

@ -17,9 +17,8 @@ use std::{
use crossbeam_channel::{never, select, unbounded, RecvError, Sender};
use lsp_server::{Connection, ErrorCode, Message, Notification, Request, RequestId, Response};
use lsp_types::{
ClientCapabilities, NumberOrString, TextDocumentClientCapabilities, WorkDoneProgress,
WorkDoneProgressBegin, WorkDoneProgressCreateParams, WorkDoneProgressEnd,
WorkDoneProgressReport,
NumberOrString, WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressCreateParams,
WorkDoneProgressEnd, WorkDoneProgressReport,
};
use ra_flycheck::{url_from_path_with_drive_lowercasing, CheckTask};
use ra_ide::{Canceled, FileId, LibraryData, SourceRootId};
@ -31,7 +30,7 @@ use serde::{de::DeserializeOwned, Serialize};
use threadpool::ThreadPool;
use crate::{
config::get_config,
config::Config,
diagnostics::DiagnosticTask,
main_loop::{
pending_requests::{PendingRequest, PendingRequests},
@ -39,7 +38,7 @@ use crate::{
},
req,
world::{WorldSnapshot, WorldState},
Result, ServerConfig,
Result,
};
use req::ConfigurationParams;
@ -65,14 +64,7 @@ impl fmt::Display for LspError {
impl Error for LspError {}
pub fn main_loop(
ws_roots: Vec<PathBuf>,
client_caps: ClientCapabilities,
config: ServerConfig,
connection: Connection,
) -> Result<()> {
let text_document_caps = client_caps.text_document.as_ref();
let config = get_config(&config, text_document_caps);
pub fn main_loop(ws_roots: Vec<PathBuf>, config: Config, connection: Connection) -> Result<()> {
log::info!("initial config: {:#?}", config);
// Windows scheduler implements priority boosts: if thread waits for an
@ -205,7 +197,6 @@ pub fn main_loop(
&task_sender,
&libdata_sender,
&connection,
text_document_caps,
&mut world_state,
&mut loop_state,
event,
@ -316,7 +307,6 @@ fn loop_turn(
task_sender: &Sender<Task>,
libdata_sender: &Sender<LibraryData>,
connection: &Connection,
text_document_caps: Option<&TextDocumentClientCapabilities>,
world_state: &mut WorldState,
loop_state: &mut LoopState,
event: Event,
@ -370,27 +360,14 @@ fn loop_turn(
log::debug!("config update response: '{:?}", resp);
let Response { error, result, .. } = resp;
match (
error,
result.map(|result| serde_json::from_value::<Vec<ServerConfig>>(result)),
) {
match (error, result) {
(Some(err), _) => {
log::error!("failed to fetch the server settings: {:?}", err)
}
(None, Some(Ok(new_config))) => {
let new_config = new_config
.first()
.expect(
"the client is expected to always send a non-empty config data",
)
.to_owned();
world_state.update_configuration(
new_config.lru_capacity,
get_config(&new_config, text_document_caps),
);
}
(None, Some(Err(e))) => {
log::error!("failed to parse client config response: {}", e)
(None, Some(new_config)) => {
let mut config = world_state.config.clone();
config.update(&new_config);
world_state.update_configuration(config);
}
(None, None) => {
log::error!("received empty server settings response from the client")

View file

@ -11,7 +11,7 @@ use std::{
use crossbeam_channel::{unbounded, Receiver};
use lsp_types::Url;
use parking_lot::RwLock;
use ra_flycheck::{url_from_path_with_drive_lowercasing, Flycheck};
use ra_flycheck::{url_from_path_with_drive_lowercasing, Flycheck, FlycheckConfig};
use ra_ide::{
Analysis, AnalysisChange, AnalysisHost, CrateGraph, FileId, LibraryData, SourceRootId,
};
@ -30,9 +30,7 @@ use crate::{
use ra_db::ExternSourceId;
use rustc_hash::{FxHashMap, FxHashSet};
fn create_flycheck(workspaces: &[ProjectWorkspace], config: &Config) -> Option<Flycheck> {
let check_config = config.check.as_ref()?;
fn create_flycheck(workspaces: &[ProjectWorkspace], config: &FlycheckConfig) -> Option<Flycheck> {
// FIXME: Figure out the multi-workspace situation
workspaces
.iter()
@ -42,7 +40,7 @@ fn create_flycheck(workspaces: &[ProjectWorkspace], config: &Config) -> Option<F
})
.map(|cargo| {
let cargo_project_root = cargo.workspace_root().to_path_buf();
Some(Flycheck::new(check_config.clone(), cargo_project_root))
Some(Flycheck::new(config.clone(), cargo_project_root))
})
.unwrap_or_else(|| {
log::warn!("Cargo check watching only supported for cargo workspaces, disabling");
@ -187,7 +185,7 @@ impl WorldState {
});
change.set_crate_graph(crate_graph);
let flycheck = create_flycheck(&workspaces, &config);
let flycheck = config.check.as_ref().and_then(|c| create_flycheck(&workspaces, c));
let mut analysis_host = AnalysisHost::new(lru_capacity);
analysis_host.apply_change(change);
@ -204,9 +202,13 @@ impl WorldState {
}
}
pub fn update_configuration(&mut self, lru_capacity: Option<usize>, config: Config) {
self.analysis_host.update_lru_capacity(lru_capacity);
self.flycheck = create_flycheck(&self.workspaces, &config);
pub fn update_configuration(&mut self, config: Config) {
self.analysis_host.update_lru_capacity(config.lru_capacity);
if config.check != self.config.check {
self.flycheck =
config.check.as_ref().and_then(|it| create_flycheck(&self.workspaces, it));
}
self.config = config;
}

View file

@ -615,7 +615,7 @@ fn main() { message(); }
"###,
)
.with_config(|config| {
config.cargo_features.load_out_dirs_from_check = true;
config.cargo.load_out_dirs_from_check = true;
})
.server();
server.wait_until_workspace_is_loaded();

View file

@ -11,8 +11,7 @@ use lsp_server::{Connection, Message, Notification, Request};
use lsp_types::{
notification::{DidOpenTextDocument, Exit},
request::Shutdown,
ClientCapabilities, DidOpenTextDocumentParams, GotoCapability, TextDocumentClientCapabilities,
TextDocumentIdentifier, TextDocumentItem, Url, WorkDoneProgress,
DidOpenTextDocumentParams, TextDocumentIdentifier, TextDocumentItem, Url, WorkDoneProgress,
};
use serde::Serialize;
use serde_json::{to_string_pretty, Value};
@ -20,14 +19,14 @@ use tempfile::TempDir;
use test_utils::{find_mismatch, parse_fixture};
use req::{ProgressParams, ProgressParamsValue};
use rust_analyzer::{main_loop, req, ServerConfig};
use rust_analyzer::{main_loop, req, Config};
pub struct Project<'a> {
fixture: &'a str,
with_sysroot: bool,
tmp_dir: Option<TempDir>,
roots: Vec<PathBuf>,
config: Option<Box<dyn Fn(&mut ServerConfig)>>,
config: Option<Box<dyn Fn(&mut Config)>>,
}
impl<'a> Project<'a> {
@ -50,7 +49,7 @@ impl<'a> Project<'a> {
self
}
pub fn with_config(mut self, config: impl Fn(&mut ServerConfig) + 'static) -> Project<'a> {
pub fn with_config(mut self, config: impl Fn(&mut Config) + 'static) -> Project<'a> {
self.config = Some(Box::new(config));
self
}
@ -78,8 +77,11 @@ impl<'a> Project<'a> {
let roots = self.roots.into_iter().map(|root| tmp_dir.path().join(root)).collect();
let mut config =
ServerConfig { with_sysroot: self.with_sysroot, ..ServerConfig::default() };
let mut config = Config {
supports_location_link: true,
with_sysroot: self.with_sysroot,
..Config::default()
};
if let Some(f) = &self.config {
f(&mut config)
@ -105,7 +107,7 @@ pub struct Server {
impl Server {
fn new(
dir: TempDir,
config: ServerConfig,
config: Config,
roots: Vec<PathBuf>,
files: Vec<(PathBuf, String)>,
) -> Server {
@ -116,26 +118,7 @@ impl Server {
let _thread = jod_thread::Builder::new()
.name("test server".to_string())
.spawn(move || {
main_loop(
roots,
ClientCapabilities {
workspace: None,
text_document: Some(TextDocumentClientCapabilities {
definition: Some(GotoCapability {
dynamic_registration: None,
link_support: Some(true),
}),
..Default::default()
}),
window: None,
experimental: None,
},
config,
connection,
)
.unwrap()
})
.spawn(move || main_loop(roots, config, connection).unwrap())
.expect("failed to spawn a thread");
let res =