diff options
Diffstat (limited to 'src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs')
-rw-r--r-- | src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs | 1985 |
1 files changed, 1985 insertions, 0 deletions
diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs new file mode 100644 index 000000000..ac0fdf85a --- /dev/null +++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs @@ -0,0 +1,1985 @@ +//! Config used by the language server. +//! +//! We currently get this config from `initialize` LSP request, which is not the +//! best way to do it, but was the simplest thing we could implement. +//! +//! Of particular interest is the `feature_flags` hash map: while other fields +//! configure the server itself, feature flags are passed into analysis, and +//! tweak things like automatic insertion of `()` in completions. + +use std::{ffi::OsString, fmt, iter, path::PathBuf}; + +use flycheck::FlycheckConfig; +use ide::{ + AssistConfig, CallableSnippets, CompletionConfig, DiagnosticsConfig, ExprFillDefaultMode, + HighlightRelatedConfig, HoverConfig, HoverDocFormat, InlayHintsConfig, JoinLinesConfig, + Snippet, SnippetScope, +}; +use ide_db::{ + imports::insert_use::{ImportGranularity, InsertUseConfig, PrefixKind}, + SnippetCap, +}; +use itertools::Itertools; +use lsp_types::{ClientCapabilities, MarkupKind}; +use project_model::{ + CargoConfig, ProjectJson, ProjectJsonData, ProjectManifest, RustcSource, UnsetTestCrates, +}; +use rustc_hash::{FxHashMap, FxHashSet}; +use serde::{de::DeserializeOwned, Deserialize}; +use vfs::AbsPathBuf; + +use crate::{ + caps::completion_item_edit_resolve, + diagnostics::DiagnosticsMapConfig, + line_index::OffsetEncoding, + lsp_ext::{self, supports_utf8, WorkspaceSymbolSearchKind, WorkspaceSymbolSearchScope}, +}; + +mod patch_old_style; + +// Conventions for configuration keys to preserve maximal extendability without breakage: +// - Toggles (be it binary true/false or with more options in-between) should almost always suffix as `_enable` +// This has the benefit of namespaces being extensible, and if the suffix doesn't fit later it can be changed without breakage. +// - In general be wary of using the namespace of something verbatim, it prevents us from adding subkeys in the future +// - Don't use abbreviations unless really necessary +// - foo_command = overrides the subcommand, foo_overrideCommand allows full overwriting, extra args only applies for foo_command + +// Defines the server-side configuration of the rust-analyzer. We generate +// *parts* of VS Code's `package.json` config from this. +// +// However, editor specific config, which the server doesn't know about, should +// be specified directly in `package.json`. +// +// To deprecate an option by replacing it with another name use `new_name | `old_name` so that we keep +// parsing the old name. +config_data! { + struct ConfigData { + /// Placeholder expression to use for missing expressions in assists. + assist_expressionFillDefault: ExprFillDefaultDef = "\"todo\"", + + /// Warm up caches on project load. + cachePriming_enable: bool = "true", + /// How many worker threads to handle priming caches. The default `0` means to pick automatically. + cachePriming_numThreads: ParallelCachePrimingNumThreads = "0", + + /// Automatically refresh project info via `cargo metadata` on + /// `Cargo.toml` or `.cargo/config.toml` changes. + cargo_autoreload: bool = "true", + /// Run build scripts (`build.rs`) for more precise code analysis. + cargo_buildScripts_enable: bool = "true", + /// Override the command rust-analyzer uses to run build scripts and + /// build procedural macros. The command is required to output json + /// and should therefore include `--message-format=json` or a similar + /// option. + /// + /// By default, a cargo invocation will be constructed for the configured + /// targets and features, with the following base command line: + /// + /// ```bash + /// cargo check --quiet --workspace --message-format=json --all-targets + /// ``` + /// . + cargo_buildScripts_overrideCommand: Option<Vec<String>> = "null", + /// Use `RUSTC_WRAPPER=rust-analyzer` when running build scripts to + /// avoid checking unnecessary things. + cargo_buildScripts_useRustcWrapper: bool = "true", + /// List of features to activate. + /// + /// Set this to `"all"` to pass `--all-features` to cargo. + cargo_features: CargoFeatures = "[]", + /// Whether to pass `--no-default-features` to cargo. + cargo_noDefaultFeatures: bool = "false", + /// Internal config for debugging, disables loading of sysroot crates. + cargo_noSysroot: bool = "false", + /// Compilation target override (target triple). + cargo_target: Option<String> = "null", + /// Unsets `#[cfg(test)]` for the specified crates. + cargo_unsetTest: Vec<String> = "[\"core\"]", + + /// Check all targets and tests (`--all-targets`). + checkOnSave_allTargets: bool = "true", + /// Cargo command to use for `cargo check`. + checkOnSave_command: String = "\"check\"", + /// Run specified `cargo check` command for diagnostics on save. + checkOnSave_enable: bool = "true", + /// Extra arguments for `cargo check`. + checkOnSave_extraArgs: Vec<String> = "[]", + /// List of features to activate. Defaults to + /// `#rust-analyzer.cargo.features#`. + /// + /// Set to `"all"` to pass `--all-features` to Cargo. + checkOnSave_features: Option<CargoFeatures> = "null", + /// Whether to pass `--no-default-features` to Cargo. Defaults to + /// `#rust-analyzer.cargo.noDefaultFeatures#`. + checkOnSave_noDefaultFeatures: Option<bool> = "null", + /// Override the command rust-analyzer uses instead of `cargo check` for + /// diagnostics on save. The command is required to output json and + /// should therefor include `--message-format=json` or a similar option. + /// + /// If you're changing this because you're using some tool wrapping + /// Cargo, you might also want to change + /// `#rust-analyzer.cargo.buildScripts.overrideCommand#`. + /// + /// An example command would be: + /// + /// ```bash + /// cargo check --workspace --message-format=json --all-targets + /// ``` + /// . + checkOnSave_overrideCommand: Option<Vec<String>> = "null", + /// Check for a specific target. Defaults to + /// `#rust-analyzer.cargo.target#`. + checkOnSave_target: Option<String> = "null", + + /// Toggles the additional completions that automatically add imports when completed. + /// Note that your client must specify the `additionalTextEdits` LSP client capability to truly have this feature enabled. + completion_autoimport_enable: bool = "true", + /// Toggles the additional completions that automatically show method calls and field accesses + /// with `self` prefixed to them when inside a method. + completion_autoself_enable: bool = "true", + /// Whether to add parenthesis and argument snippets when completing function. + completion_callable_snippets: CallableCompletionDef = "\"fill_arguments\"", + /// Whether to show postfix snippets like `dbg`, `if`, `not`, etc. + completion_postfix_enable: bool = "true", + /// Enables completions of private items and fields that are defined in the current workspace even if they are not visible at the current position. + completion_privateEditable_enable: bool = "false", + /// Custom completion snippets. + // NOTE: Keep this list in sync with the feature docs of user snippets. + completion_snippets_custom: FxHashMap<String, SnippetDef> = r#"{ + "Arc::new": { + "postfix": "arc", + "body": "Arc::new(${receiver})", + "requires": "std::sync::Arc", + "description": "Put the expression into an `Arc`", + "scope": "expr" + }, + "Rc::new": { + "postfix": "rc", + "body": "Rc::new(${receiver})", + "requires": "std::rc::Rc", + "description": "Put the expression into an `Rc`", + "scope": "expr" + }, + "Box::pin": { + "postfix": "pinbox", + "body": "Box::pin(${receiver})", + "requires": "std::boxed::Box", + "description": "Put the expression into a pinned `Box`", + "scope": "expr" + }, + "Ok": { + "postfix": "ok", + "body": "Ok(${receiver})", + "description": "Wrap the expression in a `Result::Ok`", + "scope": "expr" + }, + "Err": { + "postfix": "err", + "body": "Err(${receiver})", + "description": "Wrap the expression in a `Result::Err`", + "scope": "expr" + }, + "Some": { + "postfix": "some", + "body": "Some(${receiver})", + "description": "Wrap the expression in an `Option::Some`", + "scope": "expr" + } + }"#, + + /// List of rust-analyzer diagnostics to disable. + diagnostics_disabled: FxHashSet<String> = "[]", + /// Whether to show native rust-analyzer diagnostics. + diagnostics_enable: bool = "true", + /// Whether to show experimental rust-analyzer diagnostics that might + /// have more false positives than usual. + diagnostics_experimental_enable: bool = "false", + /// Map of prefixes to be substituted when parsing diagnostic file paths. + /// This should be the reverse mapping of what is passed to `rustc` as `--remap-path-prefix`. + diagnostics_remapPrefix: FxHashMap<String, String> = "{}", + /// List of warnings that should be displayed with hint severity. + /// + /// The warnings will be indicated by faded text or three dots in code + /// and will not show up in the `Problems Panel`. + diagnostics_warningsAsHint: Vec<String> = "[]", + /// List of warnings that should be displayed with info severity. + /// + /// The warnings will be indicated by a blue squiggly underline in code + /// and a blue icon in the `Problems Panel`. + diagnostics_warningsAsInfo: Vec<String> = "[]", + + /// These directories will be ignored by rust-analyzer. They are + /// relative to the workspace root, and globs are not supported. You may + /// also need to add the folders to Code's `files.watcherExclude`. + files_excludeDirs: Vec<PathBuf> = "[]", + /// Controls file watching implementation. + files_watcher: FilesWatcherDef = "\"client\"", + + /// Enables highlighting of related references while the cursor is on `break`, `loop`, `while`, or `for` keywords. + highlightRelated_breakPoints_enable: bool = "true", + /// Enables highlighting of all exit points while the cursor is on any `return`, `?`, `fn`, or return type arrow (`->`). + highlightRelated_exitPoints_enable: bool = "true", + /// Enables highlighting of related references while the cursor is on any identifier. + highlightRelated_references_enable: bool = "true", + /// Enables highlighting of all break points for a loop or block context while the cursor is on any `async` or `await` keywords. + highlightRelated_yieldPoints_enable: bool = "true", + + /// Whether to show `Debug` action. Only applies when + /// `#rust-analyzer.hover.actions.enable#` is set. + hover_actions_debug_enable: bool = "true", + /// Whether to show HoverActions in Rust files. + hover_actions_enable: bool = "true", + /// Whether to show `Go to Type Definition` action. Only applies when + /// `#rust-analyzer.hover.actions.enable#` is set. + hover_actions_gotoTypeDef_enable: bool = "true", + /// Whether to show `Implementations` action. Only applies when + /// `#rust-analyzer.hover.actions.enable#` is set. + hover_actions_implementations_enable: bool = "true", + /// Whether to show `References` action. Only applies when + /// `#rust-analyzer.hover.actions.enable#` is set. + hover_actions_references_enable: bool = "false", + /// Whether to show `Run` action. Only applies when + /// `#rust-analyzer.hover.actions.enable#` is set. + hover_actions_run_enable: bool = "true", + + /// Whether to show documentation on hover. + hover_documentation_enable: bool = "true", + /// Use markdown syntax for links in hover. + hover_links_enable: bool = "true", + + /// Whether to enforce the import granularity setting for all files. If set to false rust-analyzer will try to keep import styles consistent per file. + imports_granularity_enforce: bool = "false", + /// How imports should be grouped into use statements. + imports_granularity_group: ImportGranularityDef = "\"crate\"", + /// Group inserted imports by the https://rust-analyzer.github.io/manual.html#auto-import[following order]. Groups are separated by newlines. + imports_group_enable: bool = "true", + /// Whether to allow import insertion to merge new imports into single path glob imports like `use std::fmt::*;`. + imports_merge_glob: bool = "true", + /// The path structure for newly inserted paths to use. + imports_prefix: ImportPrefixDef = "\"plain\"", + + /// Whether to show inlay type hints for binding modes. + inlayHints_bindingModeHints_enable: bool = "false", + /// Whether to show inlay type hints for method chains. + inlayHints_chainingHints_enable: bool = "true", + /// Whether to show inlay hints after a closing `}` to indicate what item it belongs to. + inlayHints_closingBraceHints_enable: bool = "true", + /// Minimum number of lines required before the `}` until the hint is shown (set to 0 or 1 + /// to always show them). + inlayHints_closingBraceHints_minLines: usize = "25", + /// Whether to show inlay type hints for return types of closures. + inlayHints_closureReturnTypeHints_enable: ClosureReturnTypeHintsDef = "\"never\"", + /// Whether to show inlay type hints for elided lifetimes in function signatures. + inlayHints_lifetimeElisionHints_enable: LifetimeElisionDef = "\"never\"", + /// Whether to prefer using parameter names as the name for elided lifetime hints if possible. + inlayHints_lifetimeElisionHints_useParameterNames: bool = "false", + /// Maximum length for inlay hints. Set to null to have an unlimited length. + inlayHints_maxLength: Option<usize> = "25", + /// Whether to show function parameter name inlay hints at the call + /// site. + inlayHints_parameterHints_enable: bool = "true", + /// Whether to show inlay type hints for compiler inserted reborrows. + inlayHints_reborrowHints_enable: ReborrowHintsDef = "\"never\"", + /// Whether to render leading colons for type hints, and trailing colons for parameter hints. + inlayHints_renderColons: bool = "true", + /// Whether to show inlay type hints for variables. + inlayHints_typeHints_enable: bool = "true", + /// Whether to hide inlay type hints for `let` statements that initialize to a closure. + /// Only applies to closures with blocks, same as `#rust-analyzer.inlayHints.closureReturnTypeHints.enable#`. + inlayHints_typeHints_hideClosureInitialization: bool = "false", + /// Whether to hide inlay type hints for constructors. + inlayHints_typeHints_hideNamedConstructor: bool = "false", + + /// Join lines merges consecutive declaration and initialization of an assignment. + joinLines_joinAssignments: bool = "true", + /// Join lines inserts else between consecutive ifs. + joinLines_joinElseIf: bool = "true", + /// Join lines removes trailing commas. + joinLines_removeTrailingComma: bool = "true", + /// Join lines unwraps trivial blocks. + joinLines_unwrapTrivialBlock: bool = "true", + + /// Whether to show `Debug` lens. Only applies when + /// `#rust-analyzer.lens.enable#` is set. + lens_debug_enable: bool = "true", + /// Whether to show CodeLens in Rust files. + lens_enable: bool = "true", + /// Internal config: use custom client-side commands even when the + /// client doesn't set the corresponding capability. + lens_forceCustomCommands: bool = "true", + /// Whether to show `Implementations` lens. Only applies when + /// `#rust-analyzer.lens.enable#` is set. + lens_implementations_enable: bool = "true", + /// Whether to show `References` lens for Struct, Enum, and Union. + /// Only applies when `#rust-analyzer.lens.enable#` is set. + lens_references_adt_enable: bool = "false", + /// Whether to show `References` lens for Enum Variants. + /// Only applies when `#rust-analyzer.lens.enable#` is set. + lens_references_enumVariant_enable: bool = "false", + /// Whether to show `Method References` lens. Only applies when + /// `#rust-analyzer.lens.enable#` is set. + lens_references_method_enable: bool = "false", + /// Whether to show `References` lens for Trait. + /// Only applies when `#rust-analyzer.lens.enable#` is set. + lens_references_trait_enable: bool = "false", + /// Whether to show `Run` lens. Only applies when + /// `#rust-analyzer.lens.enable#` is set. + lens_run_enable: bool = "true", + + /// Disable project auto-discovery in favor of explicitly specified set + /// of projects. + /// + /// Elements must be paths pointing to `Cargo.toml`, + /// `rust-project.json`, or JSON objects in `rust-project.json` format. + linkedProjects: Vec<ManifestOrProjectJson> = "[]", + + /// Number of syntax trees rust-analyzer keeps in memory. Defaults to 128. + lru_capacity: Option<usize> = "null", + + /// Whether to show `can't find Cargo.toml` error message. + notifications_cargoTomlNotFound: bool = "true", + + /// Expand attribute macros. Requires `#rust-analyzer.procMacro.enable#` to be set. + procMacro_attributes_enable: bool = "true", + /// Enable support for procedural macros, implies `#rust-analyzer.cargo.buildScripts.enable#`. + procMacro_enable: bool = "true", + /// These proc-macros will be ignored when trying to expand them. + /// + /// This config takes a map of crate names with the exported proc-macro names to ignore as values. + procMacro_ignored: FxHashMap<Box<str>, Box<[Box<str>]>> = "{}", + /// Internal config, path to proc-macro server executable (typically, + /// this is rust-analyzer itself, but we override this in tests). + procMacro_server: Option<PathBuf> = "null", + + /// Command to be executed instead of 'cargo' for runnables. + runnables_command: Option<String> = "null", + /// Additional arguments to be passed to cargo for runnables such as + /// tests or binaries. For example, it may be `--release`. + runnables_extraArgs: Vec<String> = "[]", + + /// Path to the Cargo.toml of the rust compiler workspace, for usage in rustc_private + /// projects, or "discover" to try to automatically find it if the `rustc-dev` component + /// is installed. + /// + /// Any project which uses rust-analyzer with the rustcPrivate + /// crates must set `[package.metadata.rust-analyzer] rustc_private=true` to use it. + /// + /// This option does not take effect until rust-analyzer is restarted. + rustc_source: Option<String> = "null", + + /// Additional arguments to `rustfmt`. + rustfmt_extraArgs: Vec<String> = "[]", + /// Advanced option, fully override the command rust-analyzer uses for + /// formatting. + rustfmt_overrideCommand: Option<Vec<String>> = "null", + /// Enables the use of rustfmt's unstable range formatting command for the + /// `textDocument/rangeFormatting` request. The rustfmt option is unstable and only + /// available on a nightly build. + rustfmt_rangeFormatting_enable: bool = "false", + + /// Use semantic tokens for strings. + /// + /// In some editors (e.g. vscode) semantic tokens override other highlighting grammars. + /// By disabling semantic tokens for strings, other grammars can be used to highlight + /// their contents. + semanticHighlighting_strings_enable: bool = "true", + + /// Show full signature of the callable. Only shows parameters if disabled. + signatureInfo_detail: SignatureDetail = "\"full\"", + /// Show documentation. + signatureInfo_documentation_enable: bool = "true", + + /// Whether to insert closing angle brackets when typing an opening angle bracket of a generic argument list. + typing_autoClosingAngleBrackets_enable: bool = "false", + + /// Workspace symbol search kind. + workspace_symbol_search_kind: WorkspaceSymbolSearchKindDef = "\"only_types\"", + /// Limits the number of items returned from a workspace symbol search (Defaults to 128). + /// Some clients like vs-code issue new searches on result filtering and don't require all results to be returned in the initial search. + /// Other clients requires all results upfront and might require a higher limit. + workspace_symbol_search_limit: usize = "128", + /// Workspace symbol search scope. + workspace_symbol_search_scope: WorkspaceSymbolSearchScopeDef = "\"workspace\"", + } +} + +impl Default for ConfigData { + fn default() -> Self { + ConfigData::from_json(serde_json::Value::Null, &mut Vec::new()) + } +} + +#[derive(Debug, Clone)] +pub struct Config { + pub discovered_projects: Option<Vec<ProjectManifest>>, + caps: lsp_types::ClientCapabilities, + root_path: AbsPathBuf, + data: ConfigData, + detached_files: Vec<AbsPathBuf>, + snippets: Vec<Snippet>, +} + +type ParallelCachePrimingNumThreads = u8; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum LinkedProject { + ProjectManifest(ProjectManifest), + InlineJsonProject(ProjectJson), +} + +impl From<ProjectManifest> for LinkedProject { + fn from(v: ProjectManifest) -> Self { + LinkedProject::ProjectManifest(v) + } +} + +impl From<ProjectJson> for LinkedProject { + fn from(v: ProjectJson) -> Self { + LinkedProject::InlineJsonProject(v) + } +} + +pub struct CallInfoConfig { + pub params_only: bool, + pub docs: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LensConfig { + // runnables + pub run: bool, + pub debug: bool, + + // implementations + pub implementations: bool, + + // references + pub method_refs: bool, + pub refs_adt: bool, // for Struct, Enum, Union and Trait + pub refs_trait: bool, // for Struct, Enum, Union and Trait + pub enum_variant_refs: bool, +} + +impl LensConfig { + pub fn any(&self) -> bool { + self.run + || self.debug + || self.implementations + || self.method_refs + || self.refs_adt + || self.refs_trait + || self.enum_variant_refs + } + + pub fn none(&self) -> bool { + !self.any() + } + + pub fn runnable(&self) -> bool { + self.run || self.debug + } + + pub fn references(&self) -> bool { + self.method_refs || self.refs_adt || self.refs_trait || self.enum_variant_refs + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HoverActionsConfig { + pub implementations: bool, + pub references: bool, + pub run: bool, + pub debug: bool, + pub goto_type_def: bool, +} + +impl HoverActionsConfig { + pub const NO_ACTIONS: Self = Self { + implementations: false, + references: false, + run: false, + debug: false, + goto_type_def: false, + }; + + pub fn any(&self) -> bool { + self.implementations || self.references || self.runnable() || self.goto_type_def + } + + pub fn none(&self) -> bool { + !self.any() + } + + pub fn runnable(&self) -> bool { + self.run || self.debug + } +} + +#[derive(Debug, Clone)] +pub struct FilesConfig { + pub watcher: FilesWatcher, + pub exclude: Vec<AbsPathBuf>, +} + +#[derive(Debug, Clone)] +pub enum FilesWatcher { + Client, + Server, +} + +#[derive(Debug, Clone)] +pub struct NotificationsConfig { + pub cargo_toml_not_found: bool, +} + +#[derive(Debug, Clone)] +pub enum RustfmtConfig { + Rustfmt { extra_args: Vec<String>, enable_range_formatting: bool }, + CustomCommand { command: String, args: Vec<String> }, +} + +/// Configuration for runnable items, such as `main` function or tests. +#[derive(Debug, Clone)] +pub struct RunnablesConfig { + /// Custom command to be executed instead of `cargo` for runnables. + pub override_cargo: Option<String>, + /// Additional arguments for the `cargo`, e.g. `--release`. + pub cargo_extra_args: Vec<String>, +} + +/// Configuration for workspace symbol search requests. +#[derive(Debug, Clone)] +pub struct WorkspaceSymbolConfig { + /// In what scope should the symbol be searched in. + pub search_scope: WorkspaceSymbolSearchScope, + /// What kind of symbol is being searched for. + pub search_kind: WorkspaceSymbolSearchKind, + /// How many items are returned at most. + pub search_limit: usize, +} + +pub struct ClientCommandsConfig { + pub run_single: bool, + pub debug_single: bool, + pub show_reference: bool, + pub goto_location: bool, + pub trigger_parameter_hints: bool, +} + +#[derive(Debug)] +pub struct ConfigUpdateError { + errors: Vec<(String, serde_json::Error)>, +} + +impl fmt::Display for ConfigUpdateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let errors = self.errors.iter().format_with("\n", |(key, e), f| { + f(key)?; + f(&": ")?; + f(e) + }); + write!( + f, + "rust-analyzer found {} invalid config value{}:\n{}", + self.errors.len(), + if self.errors.len() == 1 { "" } else { "s" }, + errors + ) + } +} + +impl Config { + pub fn new(root_path: AbsPathBuf, caps: ClientCapabilities) -> Self { + Config { + caps, + data: ConfigData::default(), + detached_files: Vec::new(), + discovered_projects: None, + root_path, + snippets: Default::default(), + } + } + + pub fn update(&mut self, mut json: serde_json::Value) -> Result<(), ConfigUpdateError> { + tracing::info!("updating config from JSON: {:#}", json); + if json.is_null() || json.as_object().map_or(false, |it| it.is_empty()) { + return Ok(()); + } + let mut errors = Vec::new(); + self.detached_files = + get_field::<Vec<PathBuf>>(&mut json, &mut errors, "detachedFiles", None, "[]") + .into_iter() + .map(AbsPathBuf::assert) + .collect(); + patch_old_style::patch_json_for_outdated_configs(&mut json); + self.data = ConfigData::from_json(json, &mut errors); + tracing::debug!("deserialized config data: {:#?}", self.data); + self.snippets.clear(); + for (name, def) in self.data.completion_snippets_custom.iter() { + if def.prefix.is_empty() && def.postfix.is_empty() { + continue; + } + let scope = match def.scope { + SnippetScopeDef::Expr => SnippetScope::Expr, + SnippetScopeDef::Type => SnippetScope::Type, + SnippetScopeDef::Item => SnippetScope::Item, + }; + match Snippet::new( + &def.prefix, + &def.postfix, + &def.body, + def.description.as_ref().unwrap_or(name), + &def.requires, + scope, + ) { + Some(snippet) => self.snippets.push(snippet), + None => errors.push(( + format!("snippet {name} is invalid"), + <serde_json::Error as serde::de::Error>::custom( + "snippet path is invalid or triggers are missing", + ), + )), + } + } + + self.validate(&mut errors); + + if errors.is_empty() { + Ok(()) + } else { + Err(ConfigUpdateError { errors }) + } + } + + fn validate(&self, error_sink: &mut Vec<(String, serde_json::Error)>) { + use serde::de::Error; + if self.data.checkOnSave_command.is_empty() { + error_sink.push(( + "/checkOnSave/command".to_string(), + serde_json::Error::custom("expected a non-empty string"), + )); + } + } + + pub fn json_schema() -> serde_json::Value { + ConfigData::json_schema() + } + + pub fn root_path(&self) -> &AbsPathBuf { + &self.root_path + } + + pub fn caps(&self) -> &lsp_types::ClientCapabilities { + &self.caps + } + + pub fn detached_files(&self) -> &[AbsPathBuf] { + &self.detached_files + } +} + +macro_rules! try_ { + ($expr:expr) => { + || -> _ { Some($expr) }() + }; +} +macro_rules! try_or { + ($expr:expr, $or:expr) => { + try_!($expr).unwrap_or($or) + }; +} + +macro_rules! try_or_def { + ($expr:expr) => { + try_!($expr).unwrap_or_default() + }; +} + +impl Config { + pub fn linked_projects(&self) -> Vec<LinkedProject> { + match self.data.linkedProjects.as_slice() { + [] => match self.discovered_projects.as_ref() { + Some(discovered_projects) => { + let exclude_dirs: Vec<_> = self + .data + .files_excludeDirs + .iter() + .map(|p| self.root_path.join(p)) + .collect(); + discovered_projects + .iter() + .filter(|p| { + let (ProjectManifest::ProjectJson(path) + | ProjectManifest::CargoToml(path)) = p; + !exclude_dirs.iter().any(|p| path.starts_with(p)) + }) + .cloned() + .map(LinkedProject::from) + .collect() + } + None => Vec::new(), + }, + linked_projects => linked_projects + .iter() + .filter_map(|linked_project| match linked_project { + ManifestOrProjectJson::Manifest(it) => { + let path = self.root_path.join(it); + ProjectManifest::from_manifest_file(path) + .map_err(|e| tracing::error!("failed to load linked project: {}", e)) + .ok() + .map(Into::into) + } + ManifestOrProjectJson::ProjectJson(it) => { + Some(ProjectJson::new(&self.root_path, it.clone()).into()) + } + }) + .collect(), + } + } + + pub fn did_save_text_document_dynamic_registration(&self) -> bool { + let caps = try_or_def!(self.caps.text_document.as_ref()?.synchronization.clone()?); + caps.did_save == Some(true) && caps.dynamic_registration == Some(true) + } + + pub fn did_change_watched_files_dynamic_registration(&self) -> bool { + try_or_def!( + self.caps.workspace.as_ref()?.did_change_watched_files.as_ref()?.dynamic_registration? + ) + } + + pub fn prefill_caches(&self) -> bool { + self.data.cachePriming_enable + } + + pub fn location_link(&self) -> bool { + try_or_def!(self.caps.text_document.as_ref()?.definition?.link_support?) + } + + pub fn line_folding_only(&self) -> bool { + try_or_def!(self.caps.text_document.as_ref()?.folding_range.as_ref()?.line_folding_only?) + } + + pub fn hierarchical_symbols(&self) -> bool { + try_or_def!( + self.caps + .text_document + .as_ref()? + .document_symbol + .as_ref()? + .hierarchical_document_symbol_support? + ) + } + + pub fn code_action_literals(&self) -> bool { + try_!(self + .caps + .text_document + .as_ref()? + .code_action + .as_ref()? + .code_action_literal_support + .as_ref()?) + .is_some() + } + + pub fn work_done_progress(&self) -> bool { + try_or_def!(self.caps.window.as_ref()?.work_done_progress?) + } + + pub fn will_rename(&self) -> bool { + try_or_def!(self.caps.workspace.as_ref()?.file_operations.as_ref()?.will_rename?) + } + + pub fn change_annotation_support(&self) -> bool { + try_!(self + .caps + .workspace + .as_ref()? + .workspace_edit + .as_ref()? + .change_annotation_support + .as_ref()?) + .is_some() + } + + pub fn code_action_resolve(&self) -> bool { + try_or_def!(self + .caps + .text_document + .as_ref()? + .code_action + .as_ref()? + .resolve_support + .as_ref()? + .properties + .as_slice()) + .iter() + .any(|it| it == "edit") + } + + pub fn signature_help_label_offsets(&self) -> bool { + try_or_def!( + self.caps + .text_document + .as_ref()? + .signature_help + .as_ref()? + .signature_information + .as_ref()? + .parameter_information + .as_ref()? + .label_offset_support? + ) + } + + pub fn completion_label_details_support(&self) -> bool { + try_!(self + .caps + .text_document + .as_ref()? + .completion + .as_ref()? + .completion_item + .as_ref()? + .label_details_support + .as_ref()?) + .is_some() + } + + pub fn offset_encoding(&self) -> OffsetEncoding { + if supports_utf8(&self.caps) { + OffsetEncoding::Utf8 + } else { + OffsetEncoding::Utf16 + } + } + + fn experimental(&self, index: &'static str) -> bool { + try_or_def!(self.caps.experimental.as_ref()?.get(index)?.as_bool()?) + } + + pub fn code_action_group(&self) -> bool { + self.experimental("codeActionGroup") + } + + pub fn server_status_notification(&self) -> bool { + self.experimental("serverStatusNotification") + } + + pub fn publish_diagnostics(&self) -> bool { + self.data.diagnostics_enable + } + + pub fn diagnostics(&self) -> DiagnosticsConfig { + DiagnosticsConfig { + proc_attr_macros_enabled: self.expand_proc_attr_macros(), + proc_macros_enabled: self.data.procMacro_enable, + disable_experimental: !self.data.diagnostics_experimental_enable, + disabled: self.data.diagnostics_disabled.clone(), + expr_fill_default: match self.data.assist_expressionFillDefault { + ExprFillDefaultDef::Todo => ExprFillDefaultMode::Todo, + ExprFillDefaultDef::Default => ExprFillDefaultMode::Default, + }, + } + } + + pub fn diagnostics_map(&self) -> DiagnosticsMapConfig { + DiagnosticsMapConfig { + remap_prefix: self.data.diagnostics_remapPrefix.clone(), + warnings_as_info: self.data.diagnostics_warningsAsInfo.clone(), + warnings_as_hint: self.data.diagnostics_warningsAsHint.clone(), + } + } + + pub fn lru_capacity(&self) -> Option<usize> { + self.data.lru_capacity + } + + pub fn proc_macro_srv(&self) -> Option<(AbsPathBuf, Vec<OsString>)> { + if !self.data.procMacro_enable { + return None; + } + let path = match &self.data.procMacro_server { + Some(it) => self.root_path.join(it), + None => AbsPathBuf::assert(std::env::current_exe().ok()?), + }; + Some((path, vec!["proc-macro".into()])) + } + + pub fn dummy_replacements(&self) -> &FxHashMap<Box<str>, Box<[Box<str>]>> { + &self.data.procMacro_ignored + } + + pub fn expand_proc_attr_macros(&self) -> bool { + self.data.procMacro_enable && self.data.procMacro_attributes_enable + } + + pub fn files(&self) -> FilesConfig { + FilesConfig { + watcher: match self.data.files_watcher { + FilesWatcherDef::Client if self.did_change_watched_files_dynamic_registration() => { + FilesWatcher::Client + } + _ => FilesWatcher::Server, + }, + exclude: self.data.files_excludeDirs.iter().map(|it| self.root_path.join(it)).collect(), + } + } + + pub fn notifications(&self) -> NotificationsConfig { + NotificationsConfig { cargo_toml_not_found: self.data.notifications_cargoTomlNotFound } + } + + pub fn cargo_autoreload(&self) -> bool { + self.data.cargo_autoreload + } + + pub fn run_build_scripts(&self) -> bool { + self.data.cargo_buildScripts_enable || self.data.procMacro_enable + } + + pub fn cargo(&self) -> CargoConfig { + let rustc_source = self.data.rustc_source.as_ref().map(|rustc_src| { + if rustc_src == "discover" { + RustcSource::Discover + } else { + RustcSource::Path(self.root_path.join(rustc_src)) + } + }); + + CargoConfig { + no_default_features: self.data.cargo_noDefaultFeatures, + all_features: matches!(self.data.cargo_features, CargoFeatures::All), + features: match &self.data.cargo_features { + CargoFeatures::All => vec![], + CargoFeatures::Listed(it) => it.clone(), + }, + target: self.data.cargo_target.clone(), + no_sysroot: self.data.cargo_noSysroot, + rustc_source, + unset_test_crates: UnsetTestCrates::Only(self.data.cargo_unsetTest.clone()), + wrap_rustc_in_build_scripts: self.data.cargo_buildScripts_useRustcWrapper, + run_build_script_command: self.data.cargo_buildScripts_overrideCommand.clone(), + } + } + + pub fn rustfmt(&self) -> RustfmtConfig { + match &self.data.rustfmt_overrideCommand { + Some(args) if !args.is_empty() => { + let mut args = args.clone(); + let command = args.remove(0); + RustfmtConfig::CustomCommand { command, args } + } + Some(_) | None => RustfmtConfig::Rustfmt { + extra_args: self.data.rustfmt_extraArgs.clone(), + enable_range_formatting: self.data.rustfmt_rangeFormatting_enable, + }, + } + } + + pub fn flycheck(&self) -> Option<FlycheckConfig> { + if !self.data.checkOnSave_enable { + return None; + } + let flycheck_config = match &self.data.checkOnSave_overrideCommand { + Some(args) if !args.is_empty() => { + let mut args = args.clone(); + let command = args.remove(0); + FlycheckConfig::CustomCommand { command, args } + } + Some(_) | None => FlycheckConfig::CargoCommand { + command: self.data.checkOnSave_command.clone(), + target_triple: self + .data + .checkOnSave_target + .clone() + .or_else(|| self.data.cargo_target.clone()), + all_targets: self.data.checkOnSave_allTargets, + no_default_features: self + .data + .checkOnSave_noDefaultFeatures + .unwrap_or(self.data.cargo_noDefaultFeatures), + all_features: matches!( + self.data.checkOnSave_features.as_ref().unwrap_or(&self.data.cargo_features), + CargoFeatures::All + ), + features: match self + .data + .checkOnSave_features + .clone() + .unwrap_or_else(|| self.data.cargo_features.clone()) + { + CargoFeatures::All => vec![], + CargoFeatures::Listed(it) => it, + }, + extra_args: self.data.checkOnSave_extraArgs.clone(), + }, + }; + Some(flycheck_config) + } + + pub fn runnables(&self) -> RunnablesConfig { + RunnablesConfig { + override_cargo: self.data.runnables_command.clone(), + cargo_extra_args: self.data.runnables_extraArgs.clone(), + } + } + + pub fn inlay_hints(&self) -> InlayHintsConfig { + InlayHintsConfig { + render_colons: self.data.inlayHints_renderColons, + type_hints: self.data.inlayHints_typeHints_enable, + parameter_hints: self.data.inlayHints_parameterHints_enable, + chaining_hints: self.data.inlayHints_chainingHints_enable, + closure_return_type_hints: match self.data.inlayHints_closureReturnTypeHints_enable { + ClosureReturnTypeHintsDef::Always => ide::ClosureReturnTypeHints::Always, + ClosureReturnTypeHintsDef::Never => ide::ClosureReturnTypeHints::Never, + ClosureReturnTypeHintsDef::WithBlock => ide::ClosureReturnTypeHints::WithBlock, + }, + lifetime_elision_hints: match self.data.inlayHints_lifetimeElisionHints_enable { + LifetimeElisionDef::Always => ide::LifetimeElisionHints::Always, + LifetimeElisionDef::Never => ide::LifetimeElisionHints::Never, + LifetimeElisionDef::SkipTrivial => ide::LifetimeElisionHints::SkipTrivial, + }, + hide_named_constructor_hints: self.data.inlayHints_typeHints_hideNamedConstructor, + hide_closure_initialization_hints: self + .data + .inlayHints_typeHints_hideClosureInitialization, + reborrow_hints: match self.data.inlayHints_reborrowHints_enable { + ReborrowHintsDef::Always => ide::ReborrowHints::Always, + ReborrowHintsDef::Never => ide::ReborrowHints::Never, + ReborrowHintsDef::Mutable => ide::ReborrowHints::MutableOnly, + }, + binding_mode_hints: self.data.inlayHints_bindingModeHints_enable, + param_names_for_lifetime_elision_hints: self + .data + .inlayHints_lifetimeElisionHints_useParameterNames, + max_length: self.data.inlayHints_maxLength, + closing_brace_hints_min_lines: if self.data.inlayHints_closingBraceHints_enable { + Some(self.data.inlayHints_closingBraceHints_minLines) + } else { + None + }, + } + } + + fn insert_use_config(&self) -> InsertUseConfig { + InsertUseConfig { + granularity: match self.data.imports_granularity_group { + ImportGranularityDef::Preserve => ImportGranularity::Preserve, + ImportGranularityDef::Item => ImportGranularity::Item, + ImportGranularityDef::Crate => ImportGranularity::Crate, + ImportGranularityDef::Module => ImportGranularity::Module, + }, + enforce_granularity: self.data.imports_granularity_enforce, + prefix_kind: match self.data.imports_prefix { + ImportPrefixDef::Plain => PrefixKind::Plain, + ImportPrefixDef::ByCrate => PrefixKind::ByCrate, + ImportPrefixDef::BySelf => PrefixKind::BySelf, + }, + group: self.data.imports_group_enable, + skip_glob_imports: !self.data.imports_merge_glob, + } + } + + pub fn completion(&self) -> CompletionConfig { + CompletionConfig { + enable_postfix_completions: self.data.completion_postfix_enable, + enable_imports_on_the_fly: self.data.completion_autoimport_enable + && completion_item_edit_resolve(&self.caps), + enable_self_on_the_fly: self.data.completion_autoself_enable, + enable_private_editable: self.data.completion_privateEditable_enable, + callable: match self.data.completion_callable_snippets { + CallableCompletionDef::FillArguments => Some(CallableSnippets::FillArguments), + CallableCompletionDef::AddParentheses => Some(CallableSnippets::AddParentheses), + CallableCompletionDef::None => None, + }, + insert_use: self.insert_use_config(), + snippet_cap: SnippetCap::new(try_or_def!( + self.caps + .text_document + .as_ref()? + .completion + .as_ref()? + .completion_item + .as_ref()? + .snippet_support? + )), + snippets: self.snippets.clone(), + } + } + + pub fn snippet_cap(&self) -> bool { + self.experimental("snippetTextEdit") + } + + pub fn assist(&self) -> AssistConfig { + AssistConfig { + snippet_cap: SnippetCap::new(self.experimental("snippetTextEdit")), + allowed: None, + insert_use: self.insert_use_config(), + } + } + + pub fn join_lines(&self) -> JoinLinesConfig { + JoinLinesConfig { + join_else_if: self.data.joinLines_joinElseIf, + remove_trailing_comma: self.data.joinLines_removeTrailingComma, + unwrap_trivial_blocks: self.data.joinLines_unwrapTrivialBlock, + join_assignments: self.data.joinLines_joinAssignments, + } + } + + pub fn call_info(&self) -> CallInfoConfig { + CallInfoConfig { + params_only: matches!(self.data.signatureInfo_detail, SignatureDetail::Parameters), + docs: self.data.signatureInfo_documentation_enable, + } + } + + pub fn lens(&self) -> LensConfig { + LensConfig { + run: self.data.lens_enable && self.data.lens_run_enable, + debug: self.data.lens_enable && self.data.lens_debug_enable, + implementations: self.data.lens_enable && self.data.lens_implementations_enable, + method_refs: self.data.lens_enable && self.data.lens_references_method_enable, + refs_adt: self.data.lens_enable && self.data.lens_references_adt_enable, + refs_trait: self.data.lens_enable && self.data.lens_references_trait_enable, + enum_variant_refs: self.data.lens_enable + && self.data.lens_references_enumVariant_enable, + } + } + + pub fn hover_actions(&self) -> HoverActionsConfig { + let enable = self.experimental("hoverActions") && self.data.hover_actions_enable; + HoverActionsConfig { + implementations: enable && self.data.hover_actions_implementations_enable, + references: enable && self.data.hover_actions_references_enable, + run: enable && self.data.hover_actions_run_enable, + debug: enable && self.data.hover_actions_debug_enable, + goto_type_def: enable && self.data.hover_actions_gotoTypeDef_enable, + } + } + + pub fn highlighting_strings(&self) -> bool { + self.data.semanticHighlighting_strings_enable + } + + pub fn hover(&self) -> HoverConfig { + HoverConfig { + links_in_hover: self.data.hover_links_enable, + documentation: self.data.hover_documentation_enable.then(|| { + let is_markdown = try_or_def!(self + .caps + .text_document + .as_ref()? + .hover + .as_ref()? + .content_format + .as_ref()? + .as_slice()) + .contains(&MarkupKind::Markdown); + if is_markdown { + HoverDocFormat::Markdown + } else { + HoverDocFormat::PlainText + } + }), + } + } + + pub fn workspace_symbol(&self) -> WorkspaceSymbolConfig { + WorkspaceSymbolConfig { + search_scope: match self.data.workspace_symbol_search_scope { + WorkspaceSymbolSearchScopeDef::Workspace => WorkspaceSymbolSearchScope::Workspace, + WorkspaceSymbolSearchScopeDef::WorkspaceAndDependencies => { + WorkspaceSymbolSearchScope::WorkspaceAndDependencies + } + }, + search_kind: match self.data.workspace_symbol_search_kind { + WorkspaceSymbolSearchKindDef::OnlyTypes => WorkspaceSymbolSearchKind::OnlyTypes, + WorkspaceSymbolSearchKindDef::AllSymbols => WorkspaceSymbolSearchKind::AllSymbols, + }, + search_limit: self.data.workspace_symbol_search_limit, + } + } + + pub fn semantic_tokens_refresh(&self) -> bool { + try_or_def!(self.caps.workspace.as_ref()?.semantic_tokens.as_ref()?.refresh_support?) + } + + pub fn code_lens_refresh(&self) -> bool { + try_or_def!(self.caps.workspace.as_ref()?.code_lens.as_ref()?.refresh_support?) + } + + pub fn insert_replace_support(&self) -> bool { + try_or_def!( + self.caps + .text_document + .as_ref()? + .completion + .as_ref()? + .completion_item + .as_ref()? + .insert_replace_support? + ) + } + + pub fn client_commands(&self) -> ClientCommandsConfig { + let commands = + try_or!(self.caps.experimental.as_ref()?.get("commands")?, &serde_json::Value::Null); + let commands: Option<lsp_ext::ClientCommandOptions> = + serde_json::from_value(commands.clone()).ok(); + let force = commands.is_none() && self.data.lens_forceCustomCommands; + let commands = commands.map(|it| it.commands).unwrap_or_default(); + + let get = |name: &str| commands.iter().any(|it| it == name) || force; + + ClientCommandsConfig { + run_single: get("rust-analyzer.runSingle"), + debug_single: get("rust-analyzer.debugSingle"), + show_reference: get("rust-analyzer.showReferences"), + goto_location: get("rust-analyzer.gotoLocation"), + trigger_parameter_hints: get("editor.action.triggerParameterHints"), + } + } + + pub fn highlight_related(&self) -> HighlightRelatedConfig { + HighlightRelatedConfig { + references: self.data.highlightRelated_references_enable, + break_points: self.data.highlightRelated_breakPoints_enable, + exit_points: self.data.highlightRelated_exitPoints_enable, + yield_points: self.data.highlightRelated_yieldPoints_enable, + } + } + + pub fn prime_caches_num_threads(&self) -> u8 { + match self.data.cachePriming_numThreads { + 0 => num_cpus::get_physical().try_into().unwrap_or(u8::MAX), + n => n, + } + } + + pub fn typing_autoclose_angle(&self) -> bool { + self.data.typing_autoClosingAngleBrackets_enable + } +} +// Deserialization definitions + +macro_rules! create_bool_or_string_de { + ($ident:ident<$bool:literal, $string:literal>) => { + fn $ident<'de, D>(d: D) -> Result<(), D::Error> + where + D: serde::Deserializer<'de>, + { + struct V; + impl<'de> serde::de::Visitor<'de> for V { + type Value = (); + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(concat!( + stringify!($bool), + " or \"", + stringify!($string), + "\"" + )) + } + + fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + match v { + $bool => Ok(()), + _ => Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Bool(v), + &self, + )), + } + } + + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + match v { + $string => Ok(()), + _ => Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Str(v), + &self, + )), + } + } + + fn visit_enum<A>(self, a: A) -> Result<Self::Value, A::Error> + where + A: serde::de::EnumAccess<'de>, + { + use serde::de::VariantAccess; + let (variant, va) = a.variant::<&'de str>()?; + va.unit_variant()?; + match variant { + $string => Ok(()), + _ => Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Str(variant), + &self, + )), + } + } + } + d.deserialize_any(V) + } + }; +} +create_bool_or_string_de!(true_or_always<true, "always">); +create_bool_or_string_de!(false_or_never<false, "never">); + +macro_rules! named_unit_variant { + ($variant:ident) => { + pub(super) fn $variant<'de, D>(deserializer: D) -> Result<(), D::Error> + where + D: serde::Deserializer<'de>, + { + struct V; + impl<'de> serde::de::Visitor<'de> for V { + type Value = (); + fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(concat!("\"", stringify!($variant), "\"")) + } + fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> { + if value == stringify!($variant) { + Ok(()) + } else { + Err(E::invalid_value(serde::de::Unexpected::Str(value), &self)) + } + } + } + deserializer.deserialize_str(V) + } + }; +} + +mod de_unit_v { + named_unit_variant!(all); + named_unit_variant!(skip_trivial); + named_unit_variant!(mutable); + named_unit_variant!(with_block); +} + +#[derive(Deserialize, Debug, Clone, Copy)] +#[serde(rename_all = "snake_case")] +enum SnippetScopeDef { + Expr, + Item, + Type, +} + +impl Default for SnippetScopeDef { + fn default() -> Self { + SnippetScopeDef::Expr + } +} + +#[derive(Deserialize, Debug, Clone, Default)] +#[serde(default)] +struct SnippetDef { + #[serde(deserialize_with = "single_or_array")] + prefix: Vec<String>, + #[serde(deserialize_with = "single_or_array")] + postfix: Vec<String>, + description: Option<String>, + #[serde(deserialize_with = "single_or_array")] + body: Vec<String>, + #[serde(deserialize_with = "single_or_array")] + requires: Vec<String>, + scope: SnippetScopeDef, +} + +fn single_or_array<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error> +where + D: serde::Deserializer<'de>, +{ + struct SingleOrVec; + + impl<'de> serde::de::Visitor<'de> for SingleOrVec { + type Value = Vec<String>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("string or array of strings") + } + + fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + Ok(vec![value.to_owned()]) + } + + fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error> + where + A: serde::de::SeqAccess<'de>, + { + Deserialize::deserialize(serde::de::value::SeqAccessDeserializer::new(seq)) + } + } + + deserializer.deserialize_any(SingleOrVec) +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(untagged)] +enum ManifestOrProjectJson { + Manifest(PathBuf), + ProjectJson(ProjectJsonData), +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +enum ExprFillDefaultDef { + Todo, + Default, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +enum ImportGranularityDef { + Preserve, + Item, + Crate, + Module, +} + +#[derive(Deserialize, Debug, Copy, Clone)] +#[serde(rename_all = "snake_case")] +enum CallableCompletionDef { + FillArguments, + AddParentheses, + None, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(untagged)] +enum CargoFeatures { + #[serde(deserialize_with = "de_unit_v::all")] + All, + Listed(Vec<String>), +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(untagged)] +enum LifetimeElisionDef { + #[serde(deserialize_with = "true_or_always")] + Always, + #[serde(deserialize_with = "false_or_never")] + Never, + #[serde(deserialize_with = "de_unit_v::skip_trivial")] + SkipTrivial, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(untagged)] +enum ClosureReturnTypeHintsDef { + #[serde(deserialize_with = "true_or_always")] + Always, + #[serde(deserialize_with = "false_or_never")] + Never, + #[serde(deserialize_with = "de_unit_v::with_block")] + WithBlock, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(untagged)] +enum ReborrowHintsDef { + #[serde(deserialize_with = "true_or_always")] + Always, + #[serde(deserialize_with = "false_or_never")] + Never, + #[serde(deserialize_with = "de_unit_v::mutable")] + Mutable, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +enum FilesWatcherDef { + Client, + Notify, + Server, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +enum ImportPrefixDef { + Plain, + #[serde(alias = "self")] + BySelf, + #[serde(alias = "crate")] + ByCrate, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +enum WorkspaceSymbolSearchScopeDef { + Workspace, + WorkspaceAndDependencies, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +enum SignatureDetail { + Full, + Parameters, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +enum WorkspaceSymbolSearchKindDef { + OnlyTypes, + AllSymbols, +} + +macro_rules! _config_data { + (struct $name:ident { + $( + $(#[doc=$doc:literal])* + $field:ident $(| $alias:ident)*: $ty:ty = $default:expr, + )* + }) => { + #[allow(non_snake_case)] + #[derive(Debug, Clone)] + struct $name { $($field: $ty,)* } + impl $name { + fn from_json(mut json: serde_json::Value, error_sink: &mut Vec<(String, serde_json::Error)>) -> $name { + $name {$( + $field: get_field( + &mut json, + error_sink, + stringify!($field), + None$(.or(Some(stringify!($alias))))*, + $default, + ), + )*} + } + + fn json_schema() -> serde_json::Value { + schema(&[ + $({ + let field = stringify!($field); + let ty = stringify!($ty); + + (field, ty, &[$($doc),*], $default) + },)* + ]) + } + + #[cfg(test)] + fn manual() -> String { + manual(&[ + $({ + let field = stringify!($field); + let ty = stringify!($ty); + + (field, ty, &[$($doc),*], $default) + },)* + ]) + } + } + + #[test] + fn fields_are_sorted() { + [$(stringify!($field)),*].windows(2).for_each(|w| assert!(w[0] <= w[1], "{} <= {} does not hold", w[0], w[1])); + } + }; +} +use _config_data as config_data; + +fn get_field<T: DeserializeOwned>( + json: &mut serde_json::Value, + error_sink: &mut Vec<(String, serde_json::Error)>, + field: &'static str, + alias: Option<&'static str>, + default: &str, +) -> T { + let default = serde_json::from_str(default).unwrap(); + // XXX: check alias first, to work-around the VS Code where it pre-fills the + // defaults instead of sending an empty object. + alias + .into_iter() + .chain(iter::once(field)) + .find_map(move |field| { + let mut pointer = field.replace('_', "/"); + pointer.insert(0, '/'); + json.pointer_mut(&pointer).and_then(|it| match serde_json::from_value(it.take()) { + Ok(it) => Some(it), + Err(e) => { + tracing::warn!("Failed to deserialize config field at {}: {:?}", pointer, e); + error_sink.push((pointer, e)); + None + } + }) + }) + .unwrap_or(default) +} + +fn schema(fields: &[(&'static str, &'static str, &[&str], &str)]) -> serde_json::Value { + for ((f1, ..), (f2, ..)) in fields.iter().zip(&fields[1..]) { + fn key(f: &str) -> &str { + f.splitn(2, '_').next().unwrap() + } + assert!(key(f1) <= key(f2), "wrong field order: {:?} {:?}", f1, f2); + } + + let map = fields + .iter() + .map(|(field, ty, doc, default)| { + let name = field.replace('_', "."); + let name = format!("rust-analyzer.{}", name); + let props = field_props(field, ty, doc, default); + (name, props) + }) + .collect::<serde_json::Map<_, _>>(); + map.into() +} + +fn field_props(field: &str, ty: &str, doc: &[&str], default: &str) -> serde_json::Value { + let doc = doc_comment_to_string(doc); + let doc = doc.trim_end_matches('\n'); + assert!( + doc.ends_with('.') && doc.starts_with(char::is_uppercase), + "bad docs for {}: {:?}", + field, + doc + ); + let default = default.parse::<serde_json::Value>().unwrap(); + + let mut map = serde_json::Map::default(); + macro_rules! set { + ($($key:literal: $value:tt),*$(,)?) => {{$( + map.insert($key.into(), serde_json::json!($value)); + )*}}; + } + set!("markdownDescription": doc); + set!("default": default); + + match ty { + "bool" => set!("type": "boolean"), + "usize" => set!("type": "integer", "minimum": 0), + "String" => set!("type": "string"), + "Vec<String>" => set! { + "type": "array", + "items": { "type": "string" }, + }, + "Vec<PathBuf>" => set! { + "type": "array", + "items": { "type": "string" }, + }, + "FxHashSet<String>" => set! { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + }, + "FxHashMap<Box<str>, Box<[Box<str>]>>" => set! { + "type": "object", + }, + "FxHashMap<String, SnippetDef>" => set! { + "type": "object", + }, + "FxHashMap<String, String>" => set! { + "type": "object", + }, + "Option<usize>" => set! { + "type": ["null", "integer"], + "minimum": 0, + }, + "Option<String>" => set! { + "type": ["null", "string"], + }, + "Option<PathBuf>" => set! { + "type": ["null", "string"], + }, + "Option<bool>" => set! { + "type": ["null", "boolean"], + }, + "Option<Vec<String>>" => set! { + "type": ["null", "array"], + "items": { "type": "string" }, + }, + "MergeBehaviorDef" => set! { + "type": "string", + "enum": ["none", "crate", "module"], + "enumDescriptions": [ + "Do not merge imports at all.", + "Merge imports from the same crate into a single `use` statement.", + "Merge imports from the same module into a single `use` statement." + ], + }, + "ExprFillDefaultDef" => set! { + "type": "string", + "enum": ["todo", "default"], + "enumDescriptions": [ + "Fill missing expressions with the `todo` macro", + "Fill missing expressions with reasonable defaults, `new` or `default` constructors." + ], + }, + "ImportGranularityDef" => set! { + "type": "string", + "enum": ["preserve", "crate", "module", "item"], + "enumDescriptions": [ + "Do not change the granularity of any imports and preserve the original structure written by the developer.", + "Merge imports from the same crate into a single use statement. Conversely, imports from different crates are split into separate statements.", + "Merge imports from the same module into a single use statement. Conversely, imports from different modules are split into separate statements.", + "Flatten imports so that each has its own use statement." + ], + }, + "ImportPrefixDef" => set! { + "type": "string", + "enum": [ + "plain", + "self", + "crate" + ], + "enumDescriptions": [ + "Insert import paths relative to the current module, using up to one `super` prefix if the parent module contains the requested item.", + "Insert import paths relative to the current module, using up to one `super` prefix if the parent module contains the requested item. Prefixes `self` in front of the path if it starts with a module.", + "Force import paths to be absolute by always starting them with `crate` or the extern crate name they come from." + ], + }, + "Vec<ManifestOrProjectJson>" => set! { + "type": "array", + "items": { "type": ["string", "object"] }, + }, + "WorkspaceSymbolSearchScopeDef" => set! { + "type": "string", + "enum": ["workspace", "workspace_and_dependencies"], + "enumDescriptions": [ + "Search in current workspace only.", + "Search in current workspace and dependencies." + ], + }, + "WorkspaceSymbolSearchKindDef" => set! { + "type": "string", + "enum": ["only_types", "all_symbols"], + "enumDescriptions": [ + "Search for types only.", + "Search for all symbols kinds." + ], + }, + "ParallelCachePrimingNumThreads" => set! { + "type": "number", + "minimum": 0, + "maximum": 255 + }, + "LifetimeElisionDef" => set! { + "type": "string", + "enum": [ + "always", + "never", + "skip_trivial" + ], + "enumDescriptions": [ + "Always show lifetime elision hints.", + "Never show lifetime elision hints.", + "Only show lifetime elision hints if a return type is involved." + ] + }, + "ClosureReturnTypeHintsDef" => set! { + "type": "string", + "enum": [ + "always", + "never", + "with_block" + ], + "enumDescriptions": [ + "Always show type hints for return types of closures.", + "Never show type hints for return types of closures.", + "Only show type hints for return types of closures with blocks." + ] + }, + "ReborrowHintsDef" => set! { + "type": "string", + "enum": [ + "always", + "never", + "mutable" + ], + "enumDescriptions": [ + "Always show reborrow hints.", + "Never show reborrow hints.", + "Only show mutable reborrow hints." + ] + }, + "CargoFeatures" => set! { + "anyOf": [ + { + "type": "string", + "enum": [ + "all" + ], + "enumDescriptions": [ + "Pass `--all-features` to cargo", + ] + }, + { + "type": "array", + "items": { "type": "string" } + } + ], + }, + "Option<CargoFeatures>" => set! { + "anyOf": [ + { + "type": "string", + "enum": [ + "all" + ], + "enumDescriptions": [ + "Pass `--all-features` to cargo", + ] + }, + { + "type": "array", + "items": { "type": "string" } + }, + { "type": "null" } + ], + }, + "CallableCompletionDef" => set! { + "type": "string", + "enum": [ + "fill_arguments", + "add_parentheses", + "none", + ], + "enumDescriptions": [ + "Add call parentheses and pre-fill arguments.", + "Add call parentheses.", + "Do no snippet completions for callables." + ] + }, + "SignatureDetail" => set! { + "type": "string", + "enum": ["full", "parameters"], + "enumDescriptions": [ + "Show the entire signature.", + "Show only the parameters." + ], + }, + "FilesWatcherDef" => set! { + "type": "string", + "enum": ["client", "server"], + "enumDescriptions": [ + "Use the client (editor) to watch files for changes", + "Use server-side file watching", + ], + }, + _ => panic!("missing entry for {}: {}", ty, default), + } + + map.into() +} + +#[cfg(test)] +fn manual(fields: &[(&'static str, &'static str, &[&str], &str)]) -> String { + fields + .iter() + .map(|(field, _ty, doc, default)| { + let name = format!("rust-analyzer.{}", field.replace('_', ".")); + let doc = doc_comment_to_string(*doc); + if default.contains('\n') { + format!( + r#"[[{}]]{}:: ++ +-- +Default: +---- +{} +---- +{} +-- +"#, + name, name, default, doc + ) + } else { + format!("[[{}]]{} (default: `{}`)::\n+\n--\n{}--\n", name, name, default, doc) + } + }) + .collect::<String>() +} + +fn doc_comment_to_string(doc: &[&str]) -> String { + doc.iter().map(|it| it.strip_prefix(' ').unwrap_or(it)).map(|it| format!("{}\n", it)).collect() +} + +#[cfg(test)] +mod tests { + use std::fs; + + use test_utils::{ensure_file_contents, project_root}; + + use super::*; + + #[test] + fn generate_package_json_config() { + let s = Config::json_schema(); + let schema = format!("{:#}", s); + let mut schema = schema + .trim_start_matches('{') + .trim_end_matches('}') + .replace(" ", " ") + .replace('\n', "\n ") + .trim_start_matches('\n') + .trim_end() + .to_string(); + schema.push_str(",\n"); + + // Transform the asciidoc form link to markdown style. + // + // https://link[text] => [text](https://link) + let url_matches = schema.match_indices("https://"); + let mut url_offsets = url_matches.map(|(idx, _)| idx).collect::<Vec<usize>>(); + url_offsets.reverse(); + for idx in url_offsets { + let link = &schema[idx..]; + // matching on whitespace to ignore normal links + if let Some(link_end) = link.find(|c| c == ' ' || c == '[') { + if link.chars().nth(link_end) == Some('[') { + if let Some(link_text_end) = link.find(']') { + let link_text = link[link_end..(link_text_end + 1)].to_string(); + + schema.replace_range((idx + link_end)..(idx + link_text_end + 1), ""); + schema.insert(idx, '('); + schema.insert(idx + link_end + 1, ')'); + schema.insert_str(idx, &link_text); + } + } + } + } + + let package_json_path = project_root().join("editors/code/package.json"); + let mut package_json = fs::read_to_string(&package_json_path).unwrap(); + + let start_marker = " \"$generated-start\": {},\n"; + let end_marker = " \"$generated-end\": {}\n"; + + let start = package_json.find(start_marker).unwrap() + start_marker.len(); + let end = package_json.find(end_marker).unwrap(); + + let p = remove_ws(&package_json[start..end]); + let s = remove_ws(&schema); + if !p.contains(&s) { + package_json.replace_range(start..end, &schema); + ensure_file_contents(&package_json_path, &package_json) + } + } + + #[test] + fn generate_config_documentation() { + let docs_path = project_root().join("docs/user/generated_config.adoc"); + let expected = ConfigData::manual(); + ensure_file_contents(&docs_path, &expected); + } + + fn remove_ws(text: &str) -> String { + text.replace(char::is_whitespace, "") + } +} |