Repository: rust-analyzer/rust-analyzer
Pull request #3809 (2020-04-01 16:52:19)

Less config


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index c07a9614a2..9a1a056830 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -331,9 +331,9 @@ dependencies = [
 
 [[package]]
 name = "filetime"
-version = "0.2.9"
+version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f59efc38004c988e4201d11d263b8171f49a2e7ec0bdbb71773433f271504a5e"
+checksum = "1ff6d4dab0aa0c8e6346d46052e93b13a16cf847b54ed357087c35011048cc7d"
 dependencies = [
  "cfg-if",
  "libc",
@@ -842,9 +842,9 @@ checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
 
 [[package]]
 name = "proc-macro-hack"
-version = "0.5.15"
+version = "0.5.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d659fe7c6d27f25e9d80a1a094c223f5246f6a6596453e09d7229bf42750b63"
+checksum = "fcfdefadc3d57ca21cf17990a28ef4c0f7c61383a28cb7604cf4a18e6ede1420"
 
 [[package]]
 name = "proc-macro2"
diff --git a/crates/ra_flycheck/src/lib.rs b/crates/ra_flycheck/src/lib.rs
index 13494a731b..b54a30ab85 100644
--- a/crates/ra_flycheck/src/lib.rs
+++ b/crates/ra_flycheck/src/lib.rs
@@ -22,7 +22,7 @@ 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> },
diff --git a/crates/ra_project_model/src/cargo_workspace.rs b/crates/ra_project_model/src/cargo_workspace.rs
index f4fd6ad28c..c1b6e1ddc3 100644
--- a/crates/ra_project_model/src/cargo_workspace.rs
+++ b/crates/ra_project_model/src/cargo_workspace.rs
@@ -13,7 +13,6 @@ use cargo_metadata::{BuildScript, CargoOpt, Message, MetadataCommand, PackageId}
 use ra_arena::{Arena, Idx};
 use ra_db::Edition;
 use rustc_hash::FxHashMap;
-use serde::Deserialize;
 
 /// `CargoWorkspace` represents the logical structure of, well, a Cargo
 /// workspace. It pretty closely mirrors `cargo metadata` output.
@@ -43,9 +42,8 @@ impl ops::Index<Target> for CargoWorkspace {
     }
 }
 
-#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
-#[serde(rename_all = "camelCase", default)]
-pub struct CargoFeatures {
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct CargoConfig {
     /// Do not activate the `default` feature.
     pub no_default_features: bool,
 
@@ -60,9 +58,9 @@ pub struct CargoFeatures {
     pub load_out_dirs_from_check: bool,
 }
 
-impl Default for CargoFeatures {
+impl Default for CargoConfig {
     fn default() -> Self {
-        CargoFeatures {
+        CargoConfig {
             no_default_features: false,
             all_features: true,
             features: Vec::new(),
@@ -141,7 +139,7 @@ impl PackageData {
 impl CargoWorkspace {
     pub fn from_cargo_metadata(
         cargo_toml: &Path,
-        cargo_features: &CargoFeatures,
+        cargo_features: &CargoConfig,
     ) -> Result<CargoWorkspace> {
         let mut meta = MetadataCommand::new();
         meta.manifest_path(cargo_toml);
@@ -275,7 +273,7 @@ pub struct ExternResources {
 
 pub fn load_extern_resources(
     cargo_toml: &Path,
-    cargo_features: &CargoFeatures,
+    cargo_features: &CargoConfig,
 ) -> Result<ExternResources> {
     let mut cmd = Command::new(cargo_binary());
     cmd.args(&["check", "--message-format=json", "--manifest-path"]).arg(cargo_toml);
@@ -293,9 +291,8 @@ pub fn load_extern_resources(
 
     let mut res = ExternResources::default();
 
-    let stdout = String::from_utf8(output.stdout)?;
-    for line in stdout.lines() {
-        if let Ok(message) = serde_json::from_str::<cargo_metadata::Message>(&line) {
+    for message in cargo_metadata::parse_messages(output.stdout.as_slice()) {
+        if let Ok(message) = message {
             match message {
                 Message::BuildScriptExecuted(BuildScript { package_id, out_dir, .. }) => {
                     res.out_dirs.insert(package_id, out_dir);
diff --git a/crates/ra_project_model/src/lib.rs b/crates/ra_project_model/src/lib.rs
index 444d3bb3f0..dd9c80691f 100644
--- a/crates/ra_project_model/src/lib.rs
+++ b/crates/ra_project_model/src/lib.rs
@@ -19,7 +19,7 @@ use rustc_hash::FxHashMap;
 use serde_json::from_reader;
 
 pub use crate::{
-    cargo_workspace::{CargoFeatures, CargoWorkspace, Package, Target, TargetKind},
+    cargo_workspace::{CargoConfig, CargoWorkspace, Package, Target, TargetKind},
     json_project::JsonProject,
     sysroot::Sysroot,
 };
@@ -78,14 +78,14 @@ impl PackageRoot {
 }
 
 impl ProjectWorkspace {
-    pub fn discover(path: &Path, cargo_features: &CargoFeatures) -> Result<ProjectWorkspace> {
+    pub fn discover(path: &Path, cargo_features: &CargoConfig) -> Result<ProjectWorkspace> {
         ProjectWorkspace::discover_with_sysroot(path, true, cargo_features)
     }
 
     pub fn discover_with_sysroot(
         path: &Path,
         with_sysroot: bool,
-        cargo_features: &CargoFeatures,
+        cargo_features: &CargoConfig,
     ) -> Result<ProjectWorkspace> {
         match find_rust_project_json(path) {
             Some(json_path) => {
diff --git a/crates/rust-analyzer/src/bin/main.rs b/crates/rust-analyzer/src/bin/main.rs
index a744a6695d..608f4f67b2 100644
--- a/crates/rust-analyzer/src/bin/main.rs
+++ b/crates/rust-analyzer/src/bin/main.rs
@@ -4,8 +4,7 @@
 mod args;
 
 use lsp_server::Connection;
-
-use rust_analyzer::{cli, from_json, show_message, Result, ServerConfig};
+use rust_analyzer::{cli, config::Config, from_json, Result};
 
 use crate::args::HelpPrinted;
 
@@ -78,24 +77,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()?;
diff --git a/crates/rust-analyzer/src/cli/load_cargo.rs b/crates/rust-analyzer/src/cli/load_cargo.rs
index 832f04226c..2c0bde920b 100644
--- a/crates/rust-analyzer/src/cli/load_cargo.rs
+++ b/crates/rust-analyzer/src/cli/load_cargo.rs
@@ -8,7 +8,7 @@ use crossbeam_channel::{unbounded, Receiver};
 use ra_db::{ExternSourceId, FileId, SourceRootId};
 use ra_ide::{AnalysisChange, AnalysisHost};
 use ra_project_model::{
-    get_rustc_cfg_options, CargoFeatures, PackageRoot, ProcMacroClient, ProjectWorkspace,
+    get_rustc_cfg_options, CargoConfig, PackageRoot, ProcMacroClient, ProjectWorkspace,
 };
 use ra_vfs::{RootEntry, Vfs, VfsChange, VfsTask, Watch};
 use rustc_hash::{FxHashMap, FxHashSet};
@@ -29,7 +29,7 @@ pub(crate) fn load_cargo(
     let root = std::env::current_dir()?.join(root);
     let ws = ProjectWorkspace::discover(
         root.as_ref(),
-        &CargoFeatures { load_out_dirs_from_check, ..Default::default() },
+        &CargoConfig { load_out_dirs_from_check, ..Default::default() },
     )?;
 
     let mut extern_dirs = FxHashSet::default();
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index fb7895ce07..3c8f55f1e4 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -7,24 +7,36 @@
 //! configure the server itself, feature flags are passed into analysis, and
 //! tweak things like automatic insertion of `()` in completions.
 
-use rustc_hash::FxHashMap;
-
 use lsp_types::TextDocumentClientCapabilities;
 use ra_flycheck::FlycheckConfig;
-use ra_ide::InlayHintsConfig;
-use ra_project_model::CargoFeatures;
-use serde::{Deserialize, Deserializer};
+use ra_ide::{CompletionConfig, InlayHintsConfig};
+use ra_project_model::CargoConfig;
+use serde::Deserialize;
 
 #[derive(Debug, Clone)]
 pub struct Config {
+    pub client_caps: ClientCapsConfig,
     pub publish_decorations: bool,
-    pub supports_location_link: bool,
-    pub line_folding_only: bool,
+    pub publish_diagnostics: bool,
+    pub notifications: NotificationsConfig,
     pub inlay_hints: InlayHintsConfig,
+    pub completion: CompletionConfig,
+    pub call_info_full: bool,
     pub rustfmt: RustfmtConfig,
     pub check: Option<FlycheckConfig>,
     pub vscode_lldb: bool,
     pub proc_macro_srv: Option<String>,
+    pub lru_capacity: Option<usize>,
+    pub use_client_watching: bool,
+    pub exclude_globs: Vec<String>,
+    pub cargo: CargoConfig,
+    pub with_sysroot: bool,
+}
+
+#[derive(Debug, Clone)]
+pub struct NotificationsConfig {
+    pub workspace_loaded: bool,
+    pub cargo_toml_not_found: bool,
 }
 
 #[derive(Debug, Clone)]
@@ -39,148 +51,120 @@ pub enum RustfmtConfig {
     },
 }
 
-impl Default for RustfmtConfig {
-    fn default() -> Self {
-        RustfmtConfig::Rustfmt { extra_args: Vec::new() }
-    }
+#[derive(Debug, Clone, Default)]
+pub struct ClientCapsConfig {
+    pub location_link: bool,
+    pub line_folding_only: bool,
 }
 
-pub(crate) fn get_config(
-    config: &ServerConfig,
-    text_document_caps: Option<&TextDocumentClientCapabilities>,
-) -> Config {
-    Config {
-        publish_decorations: config.publish_decorations,
-        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),
-        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,
-        },
-        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
+impl Default for Config {
+    fn default() -> Self {
+        Config {
+            publish_decorations: false,
+            publish_diagnostics: true,
+            notifications: NotificationsConfig {
+                workspace_loaded: true,
+                cargo_toml_not_found: true,
+            },
+            client_caps: ClientCapsConfig::default(),
+            inlay_hints: InlayHintsConfig {
+                type_hints: true,
+                parameter_hints: true,
+                chaining_hints: true,
+                max_length: None,
+            },
+            completion: CompletionConfig {
+                enable_postfix_completions: true,
+                add_call_parenthesis: true,
+                add_call_argument_snippets: true,
+            },
+            call_info_full: true,
+            rustfmt: RustfmtConfig::Rustfmt { extra_args: Vec::new() },
+            check: Some(FlycheckConfig::CargoCommand {
+                command: "check".to_string(),
+                all_targets: true,
+                extra_args: Vec::new(),
+            }),
+            vscode_lldb: false,
+            proc_macro_srv: None,
+            lru_capacity: None,
+            use_client_watching: false,
+            exclude_globs: Vec::new(),
+            cargo: CargoConfig::default(),
+            with_sysroot: true,
+        }
     }
 }
 
-/// 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,
+impl Config {
+    #[rustfmt::skip]
+    pub fn update(&mut self, value: &serde_json::Value) {
+        log::info!("Config::update({:#})", value);
 
-    pub exclude_globs: Vec<String>,
-    #[serde(deserialize_with = "nullable_bool_false")]
-    pub use_client_watching: bool,
+        let client_caps = self.client_caps.clone();
+        *self = Default::default();
+        self.client_caps = client_caps;
 
-    pub lru_capacity: Option<usize>,
+        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);
 
-    #[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,
+        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);
 
-    /// Fine grained feature flags to disable specific features.
-    pub feature_flags: FxHashMap<String, bool>,
+        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);
+        }
 
-    pub rustfmt_args: Vec<String>,
+        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);
 
-    /// Cargo feature configurations.
-    pub cargo_features: CargoFeatures,
+        set(value, "/vscodeLldb", &mut self.vscode_lldb);
 
-    /// Enabled if the vscode_lldb extension is available.
-    pub vscode_lldb: bool,
-}
+        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);
 
-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(),
-            vscode_lldb: false,
-        }
-    }
-}
+        log::info!("Config::update() = {:#?}", self);
 
-/// 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))
-}
+        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())
+        }
 
-/// 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))
-}
+        fn set<'a, T: Deserialize<'a> + std::fmt::Debug>(value: &'a serde_json::Value, pointer: &str, slot: &mut T) {
+            if let Some(new_value) = get(value, pointer) {
+                *slot = new_value
+            }
+        }
+    }
 
-#[cfg(test)]
-mod test {
-    use super::*;
-
-    #[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()
-        );
+    pub fn update_caps(&mut self, caps: &TextDocumentClientCapabilities) {
+        if let Some(value) = caps.definition.as_ref().and_then(|it| it.link_support) {
+            self.client_caps.location_link = value;
+        }
+        if let Some(value) = caps.folding_range.as_ref().and_then(|it| it.line_folding_only) {
+            self.client_caps.line_folding_only = value
+        }
     }
 }
diff --git a/crates/rust-analyzer/src/conv.rs b/crates/rust-analyzer/src/conv.rs
index e8dc953c38..57c4c8ce55 100644
--- a/crates/rust-analyzer/src/conv.rs
+++ b/crates/rust-analyzer/src/conv.rs
@@ -579,7 +579,7 @@ impl TryConvWith<&WorldSnapshot> for (FileId, RangeInfo<Vec<NavigationTarget>>)
             .into_iter()
             .map(|nav| (file_id, RangeInfo::new(range, nav)))
             .try_conv_with_to_vec(world)?;
-        if world.config.supports_location_link {
+        if world.config.client_caps.location_link {
             Ok(links.into())
         } else {
             let locations: Vec<Location> = links
diff --git a/crates/rust-analyzer/src/feature_flags.rs b/crates/rust-analyzer/src/feature_flags.rs
deleted file mode 100644
index dbb3f50a09..0000000000
--- a/crates/rust-analyzer/src/feature_flags.rs
+++ /dev/null
@@ -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),
-        }
-    }
-}
diff --git a/crates/rust-analyzer/src/lib.rs b/crates/rust-analyzer/src/lib.rs
index e50e47b195..02953be303 100644
--- a/crates/rust-analyzer/src/lib.rs
+++ b/crates/rust-analyzer/src/lib.rs
@@ -33,18 +33,16 @@ mod conv;
 mod main_loop;
 mod markdown;
 pub mod req;
-mod config;
+pub 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::{
     caps::server_capabilities,
-    config::ServerConfig,
     main_loop::LspError,
     main_loop::{main_loop, show_message},
 };
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs
index de40e2ac2c..45ae0ad9d1 100644
--- a/crates/rust-analyzer/src/main_loop.rs
+++ b/crates/rust-analyzer/src/main_loop.rs
@@ -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,16 +30,15 @@ use serde::{de::DeserializeOwned, Serialize};
 use threadpool::ThreadPool;
 
 use crate::{
-    config::get_config,
+    config::Config,
     diagnostics::DiagnosticTask,
-    feature_flags::FeatureFlags,
     main_loop::{
         pending_requests::{PendingRequest, PendingRequests},
         subscriptions::Subscriptions,
     },
     req,
     world::{WorldSnapshot, WorldState},
-    Result, ServerConfig,
+    Result,
 };
 use req::ConfigurationParams;
 
@@ -66,29 +64,8 @@ impl fmt::Display for LspError {
 
 impl Error for LspError {}
 
-fn get_feature_flags(config: &ServerConfig, connection: &Connection) -> 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);
-            show_message(
-                req::MessageType::Error,
-                format!("unknown feature flag: {:?}", flag),
-                &connection.sender,
-            );
-        }
-    }
-    log::info!("feature_flags: {:#?}", ff);
-    ff
-}
-
-pub fn main_loop(
-    ws_roots: Vec<PathBuf>,
-    client_caps: ClientCapabilities,
-    config: ServerConfig,
-    connection: Connection,
-) -> Result<()> {
-    log::info!("server_config: {:#?}", config);
+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
     // event (like a condvar), and event fires, priority of the thread is
@@ -109,11 +86,8 @@ pub fn main_loop(
         SetThreadPriority(thread, thread_priority_above_normal);
     }
 
-    let text_document_caps = client_caps.text_document.as_ref();
     let mut loop_state = LoopState::default();
     let mut world_state = {
-        let feature_flags = get_feature_flags(&config, &connection);
-
         // FIXME: support dynamic workspace loading.
         let workspaces = {
             let mut loaded_workspaces = Vec::new();
@@ -121,7 +95,7 @@ pub fn main_loop(
                 let workspace = ra_project_model::ProjectWorkspace::discover_with_sysroot(
                     ws_root.as_path(),
                     config.with_sysroot,
-                    &config.cargo_features,
+                    &config.cargo,
                 );
                 match workspace {
                     Ok(workspace) => loaded_workspaces.push(workspace),
@@ -131,7 +105,7 @@ pub fn main_loop(
                         if let Some(ra_project_model::CargoTomlNotFoundError { .. }) =
                             e.downcast_ref()
                         {
-                            if !feature_flags.get("notifications.cargo-toml-not-found") {
+                            if !config.notifications.cargo_toml_not_found {
                                 continue;
                             }
                         }
@@ -180,8 +154,7 @@ pub fn main_loop(
             config.lru_capacity,
             &globs,
             Watch(!config.use_client_watching),
-            get_config(&config, text_document_caps),
-            feature_flags,
+            config,
         )
     };
 
@@ -224,7 +197,6 @@ pub fn main_loop(
                 &task_sender,
                 &libdata_sender,
                 &connection,
-                text_document_caps,
                 &mut world_state,
                 &mut loop_state,
                 event,
@@ -335,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,
@@ -389,28 +360,16 @@ 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),
-                                get_feature_flags(&new_config, connection),
-                            );
-                        }
-                        (None, Some(Err(e))) => {
-                            log::error!("failed to parse client config response: {}", e)
+                        (None, Some(configs)) => {
+                            if let Some(new_config) = configs.get(0) {
+                                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")
@@ -441,8 +400,8 @@ fn loop_turn(
         });
     }
 
-    let show_progress = !loop_state.workspace_loaded
-        && world_state.feature_flags.get("notifications.workspace-loaded");
+    let show_progress =
+        !loop_state.workspace_loaded && world_state.config.notifications.workspace_loaded;
 
     if !loop_state.workspace_loaded
         && loop_state.roots_scanned == loop_state.roots_total
@@ -930,7 +889,7 @@ fn update_file_notifications_on_threadpool(
     subscriptions: Vec<FileId>,
 ) {
     log::trace!("updating notifications for {:?}", subscriptions);
-    let publish_diagnostics = world.feature_flags.get("lsp.diagnostics");
+    let publish_diagnostics = world.config.publish_diagnostics;
     pool.execute(move || {
         for file_id in subscriptions {
             if publish_diagnostics {
diff --git a/crates/rust-analyzer/src/main_loop/handlers.rs b/crates/rust-analyzer/src/main_loop/handlers.rs
index bb99b38a8e..23e48c0896 100644
--- a/crates/rust-analyzer/src/main_loop/handlers.rs
+++ b/crates/rust-analyzer/src/main_loop/handlers.rs
@@ -19,8 +19,8 @@ use lsp_types::{
     TextEdit, WorkspaceEdit,
 };
 use ra_ide::{
-    Assist, AssistId, CompletionConfig, FileId, FilePosition, FileRange, Query, RangeInfo,
-    Runnable, RunnableKind, SearchScope,
+    Assist, AssistId, FileId, FilePosition, FileRange, Query, RangeInfo, Runnable, RunnableKind,
+    SearchScope,
 };
 use ra_prof::profile;
 use ra_syntax::{AstNode, SyntaxKind, TextRange, TextUnit};
@@ -426,15 +426,7 @@ pub fn handle_completion(
         return Ok(None);
     }
 
-    let config = CompletionConfig {
-        enable_postfix_completions: world.feature_flags.get("completion.enable-postfix"),
-        add_call_parenthesis: world.feature_flags.get("completion.insertion.add-call-parenthesis"),
-        add_call_argument_snippets: world
-            .feature_flags
-            .get("completion.insertion.add-argument-snippets"),
-    };
-
-    let items = match world.analysis().completions(position, &config)? {
+    let items = match world.analysis().completions(position, &world.config.completion)? {
         None => return Ok(None),
         Some(items) => items,
     };
@@ -458,7 +450,7 @@ pub fn handle_folding_range(
     let ctx = FoldConvCtx {
         text: &text,
         line_index: &line_index,
-        line_folding_only: world.config.line_folding_only,
+        line_folding_only: world.config.client_caps.line_folding_only,
     };
     let res = Some(folds.into_iter().map_conv_with(&ctx).collect());
     Ok(res)
@@ -471,7 +463,7 @@ pub fn handle_signature_help(
     let _p = profile("handle_signature_help");
     let position = params.try_conv_with(&world)?;
     if let Some(call_info) = world.analysis().call_info(position)? {
-        let concise = !world.feature_flags.get("call-info.full");
+        let concise = !world.config.call_info_full;
         let mut active_parameter = call_info.active_parameter.map(|it| it as i64);
         if concise && call_info.signature.has_self_param {
             active_parameter = active_parameter.map(|it| it.saturating_sub(1));
diff --git a/crates/rust-analyzer/src/world.rs b/crates/rust-analyzer/src/world.rs
index 124de4d8ec..5674f42ef3 100644
--- a/crates/rust-analyzer/src/world.rs
+++ b/crates/rust-analyzer/src/world.rs
@@ -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,
 };
@@ -23,7 +23,6 @@ use stdx::format_to;
 use crate::{
     config::Config,
     diagnostics::{CheckFixes, DiagnosticCollection},
-    feature_flags::FeatureFlags,
     main_loop::pending_requests::{CompletedRequest, LatestRequests},
     vfs_glob::{Glob, RustPackageFilterBuilder},
     LspError, Result,
@@ -31,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()
@@ -43,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");
@@ -59,7 +56,6 @@ fn create_flycheck(workspaces: &[ProjectWorkspace], config: &Config) -> Option<F
 #[derive(Debug)]
 pub struct WorldState {
     pub config: Config,
-    pub feature_flags: Arc<FeatureFlags>,
     pub roots: Vec<PathBuf>,
     pub workspaces: Arc<Vec<ProjectWorkspace>>,
     pub analysis_host: AnalysisHost,
@@ -73,7 +69,6 @@ pub struct WorldState {
 /// An immutable snapshot of the world's state at a point in time.
 pub struct WorldSnapshot {
     pub config: Config,
-    pub feature_flags: Arc<FeatureFlags>,
     pub workspaces: Arc<Vec<ProjectWorkspace>>,
     pub analysis: Analysis,
     pub latest_requests: Arc<RwLock<LatestRequests>>,
@@ -89,7 +84,6 @@ impl WorldState {
         exclude_globs: &[Glob],
         watch: Watch,
         config: Config,
-        feature_flags: FeatureFlags,
     ) -> WorldState {
         let mut change = AnalysisChange::new();
 
@@ -191,13 +185,12 @@ 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);
         WorldState {
             config: config,
-            feature_flags: Arc::new(feature_flags),
             roots: folder_roots,
             workspaces: Arc::new(workspaces),
             analysis_host,
@@ -209,15 +202,13 @@ impl WorldState {
         }
     }
 
-    pub fn update_configuration(
-        &mut self,
-        lru_capacity: Option<usize>,
-        config: Config,
-        feature_flags: FeatureFlags,
-    ) {
-        self.feature_flags = Arc::new(feature_flags);
-        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;
     }
 
@@ -275,7 +266,6 @@ impl WorldState {
     pub fn snapshot(&self) -> WorldSnapshot {
         WorldSnapshot {
             config: self.config.clone(),
-            feature_flags: Arc::clone(&self.feature_flags),
             workspaces: Arc::clone(&self.workspaces),
             analysis: self.analysis_host.analysis(),
             vfs: Arc::clone(&self.vfs),


Test patch:
diff --git a/crates/rust-analyzer/tests/heavy_tests/main.rs b/crates/rust-analyzer/tests/heavy_tests/main.rs
index 5af5eaad20..6388133112 100644
--- a/crates/rust-analyzer/tests/heavy_tests/main.rs
+++ b/crates/rust-analyzer/tests/heavy_tests/main.rs
@@ -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();
diff --git a/crates/rust-analyzer/tests/heavy_tests/support.rs b/crates/rust-analyzer/tests/heavy_tests/support.rs
index d8bed6d7f4..7eebedff7a 100644
--- a/crates/rust-analyzer/tests/heavy_tests/support.rs
+++ b/crates/rust-analyzer/tests/heavy_tests/support.rs
@@ -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,17 @@ use tempfile::TempDir;
 use test_utils::{find_mismatch, parse_fixture};
 
 use req::{ProgressParams, ProgressParamsValue};
-use rust_analyzer::{main_loop, req, ServerConfig};
+use rust_analyzer::{
+    config::{ClientCapsConfig, Config},
+    main_loop, req,
+};
 
 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 +52,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 +80,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 {
+            client_caps: ClientCapsConfig { location_link: true, ..Default::default() },
+            with_sysroot: self.with_sysroot,
+            ..Config::default()
+        };
 
         if let Some(f) = &self.config {
             f(&mut config)
@@ -105,7 +110,7 @@ pub struct Server {
 impl Server {
     fn new(
         dir: TempDir,
-        config: ServerConfig,
+        config: Config,
         roots: Vec<PathBuf>,
         files: Vec<(PathBuf, String)>,
     ) -> Server {
@@ -116,26 +121,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 =