]> git.lizzy.rs Git - rust.git/blob - crates/rust-analyzer/src/config.rs
Align config's API with usage
[rust.git] / crates / rust-analyzer / src / config.rs
1 //! Config used by the language server.
2 //!
3 //! We currently get this config from `initialize` LSP request, which is not the
4 //! best way to do it, but was the simplest thing we could implement.
5 //!
6 //! Of particular interest is the `feature_flags` hash map: while other fields
7 //! configure the server itself, feature flags are passed into analysis, and
8 //! tweak things like automatic insertion of `()` in completions.
9
10 use std::{convert::TryFrom, ffi::OsString, path::PathBuf};
11
12 use flycheck::FlycheckConfig;
13 use hir::PrefixKind;
14 use ide::{
15     AssistConfig, CompletionConfig, DiagnosticsConfig, HoverConfig, InlayHintsConfig,
16     InsertUseConfig,
17 };
18 use ide_db::helpers::{insert_use::MergeBehavior, SnippetCap};
19 use itertools::Itertools;
20 use lsp_types::{ClientCapabilities, MarkupKind};
21 use project_model::{CargoConfig, ProjectJson, ProjectJsonData, ProjectManifest};
22 use rustc_hash::FxHashSet;
23 use serde::{de::DeserializeOwned, Deserialize};
24 use vfs::AbsPathBuf;
25
26 use crate::{caps::completion_item_edit_resolve, diagnostics::DiagnosticsMapConfig};
27
28 config_data! {
29     struct ConfigData {
30         /// The strategy to use when inserting new imports or merging imports.
31         assist_importMergeBehaviour: MergeBehaviorDef = "\"full\"",
32         /// The path structure for newly inserted paths to use.
33         assist_importPrefix: ImportPrefixDef           = "\"plain\"",
34
35         /// Show function name and docs in parameter hints.
36         callInfo_full: bool = "true",
37
38         /// Automatically refresh project info via `cargo metadata` on
39         /// `Cargo.toml` changes.
40         cargo_autoreload: bool           = "true",
41         /// Activate all available features.
42         cargo_allFeatures: bool          = "false",
43         /// List of features to activate.
44         cargo_features: Vec<String>      = "[]",
45         /// Run `cargo check` on startup to get the correct value for package
46         /// OUT_DIRs.
47         cargo_loadOutDirsFromCheck: bool = "false",
48         /// Do not activate the `default` feature.
49         cargo_noDefaultFeatures: bool    = "false",
50         /// Compilation target (target triple).
51         cargo_target: Option<String>     = "null",
52         /// Internal config for debugging, disables loading of sysroot crates.
53         cargo_noSysroot: bool            = "false",
54
55         /// Run specified `cargo check` command for diagnostics on save.
56         checkOnSave_enable: bool                         = "true",
57         /// Check with all features (will be passed as `--all-features`).
58         /// Defaults to `#rust-analyzer.cargo.allFeatures#`.
59         checkOnSave_allFeatures: Option<bool>            = "null",
60         /// Check all targets and tests (will be passed as `--all-targets`).
61         checkOnSave_allTargets: bool                     = "true",
62         /// Cargo command to use for `cargo check`.
63         checkOnSave_command: String                      = "\"check\"",
64         /// Do not activate the `default` feature.
65         checkOnSave_noDefaultFeatures: Option<bool>      = "null",
66         /// Check for a specific target. Defaults to
67         /// `#rust-analyzer.cargo.target#`.
68         checkOnSave_target: Option<String>               = "null",
69         /// Extra arguments for `cargo check`.
70         checkOnSave_extraArgs: Vec<String>               = "[]",
71         /// List of features to activate. Defaults to
72         /// `#rust-analyzer.cargo.features#`.
73         checkOnSave_features: Option<Vec<String>>        = "null",
74         /// Advanced option, fully override the command rust-analyzer uses for
75         /// checking. The command should include `--message-format=json` or
76         /// similar option.
77         checkOnSave_overrideCommand: Option<Vec<String>> = "null",
78
79         /// Whether to add argument snippets when completing functions.
80         completion_addCallArgumentSnippets: bool = "true",
81         /// Whether to add parenthesis when completing functions.
82         completion_addCallParenthesis: bool      = "true",
83         /// Whether to show postfix snippets like `dbg`, `if`, `not`, etc.
84         completion_postfix_enable: bool          = "true",
85         /// Toggles the additional completions that automatically add imports when completed.
86         /// Note that your client must specify the `additionalTextEdits` LSP client capability to truly have this feature enabled.
87         completion_autoimport_enable: bool       = "true",
88
89         /// Whether to show native rust-analyzer diagnostics.
90         diagnostics_enable: bool                = "true",
91         /// Whether to show experimental rust-analyzer diagnostics that might
92         /// have more false positives than usual.
93         diagnostics_enableExperimental: bool    = "true",
94         /// List of rust-analyzer diagnostics to disable.
95         diagnostics_disabled: FxHashSet<String> = "[]",
96         /// List of warnings that should be displayed with info severity.\n\nThe
97         /// warnings will be indicated by a blue squiggly underline in code and
98         /// a blue icon in the `Problems Panel`.
99         diagnostics_warningsAsHint: Vec<String> = "[]",
100         /// List of warnings that should be displayed with hint severity.\n\nThe
101         /// warnings will be indicated by faded text or three dots in code and
102         /// will not show up in the `Problems Panel`.
103         diagnostics_warningsAsInfo: Vec<String> = "[]",
104
105         /// Controls file watching implementation.
106         files_watcher: String = "\"client\"",
107
108         /// Whether to show `Debug` action. Only applies when
109         /// `#rust-analyzer.hoverActions.enable#` is set.
110         hoverActions_debug: bool           = "true",
111         /// Whether to show HoverActions in Rust files.
112         hoverActions_enable: bool          = "true",
113         /// Whether to show `Go to Type Definition` action. Only applies when
114         /// `#rust-analyzer.hoverActions.enable#` is set.
115         hoverActions_gotoTypeDef: bool     = "true",
116         /// Whether to show `Implementations` action. Only applies when
117         /// `#rust-analyzer.hoverActions.enable#` is set.
118         hoverActions_implementations: bool = "true",
119         /// Whether to show `Run` action. Only applies when
120         /// `#rust-analyzer.hoverActions.enable#` is set.
121         hoverActions_run: bool             = "true",
122         /// Use markdown syntax for links in hover.
123         hoverActions_linksInHover: bool    = "true",
124
125         /// Whether to show inlay type hints for method chains.
126         inlayHints_chainingHints: bool      = "true",
127         /// Maximum length for inlay hints. Default is unlimited.
128         inlayHints_maxLength: Option<usize> = "null",
129         /// Whether to show function parameter name inlay hints at the call
130         /// site.
131         inlayHints_parameterHints: bool     = "true",
132         /// Whether to show inlay type hints for variables.
133         inlayHints_typeHints: bool          = "true",
134
135         /// Whether to show `Debug` lens. Only applies when
136         /// `#rust-analyzer.lens.enable#` is set.
137         lens_debug: bool            = "true",
138         /// Whether to show CodeLens in Rust files.
139         lens_enable: bool           = "true",
140         /// Whether to show `Implementations` lens. Only applies when
141         /// `#rust-analyzer.lens.enable#` is set.
142         lens_implementations: bool  = "true",
143         /// Whether to show `Run` lens. Only applies when
144         /// `#rust-analyzer.lens.enable#` is set.
145         lens_run: bool              = "true",
146         /// Whether to show `Method References` lens. Only applies when
147         /// `#rust-analyzer.lens.enable#` is set.
148         lens_methodReferences: bool = "false",
149
150         /// Disable project auto-discovery in favor of explicitly specified set
151         /// of projects.\n\nElements must be paths pointing to `Cargo.toml`,
152         /// `rust-project.json`, or JSON objects in `rust-project.json` format.
153         linkedProjects: Vec<ManifestOrProjectJson> = "[]",
154
155         /// Number of syntax trees rust-analyzer keeps in memory.  Defaults to 128.
156         lruCapacity: Option<usize>                 = "null",
157
158         /// Whether to show `can't find Cargo.toml` error message.
159         notifications_cargoTomlNotFound: bool      = "true",
160
161         /// Enable Proc macro support, `#rust-analyzer.cargo.loadOutDirsFromCheck#` must be
162         /// enabled.
163         procMacro_enable: bool                     = "false",
164         /// Internal config, path to proc-macro server executable (typically,
165         /// this is rust-analyzer itself, but we override this in tests).
166         procMacro_server: Option<PathBuf>          = "null",
167
168         /// Command to be executed instead of 'cargo' for runnables.
169         runnables_overrideCargo: Option<String> = "null",
170         /// Additional arguments to be passed to cargo for runnables such as
171         /// tests or binaries.\nFor example, it may be `--release`.
172         runnables_cargoExtraArgs: Vec<String>   = "[]",
173
174         /// Path to the rust compiler sources, for usage in rustc_private projects.
175         rustcSource : Option<PathBuf> = "null",
176
177         /// Additional arguments to `rustfmt`.
178         rustfmt_extraArgs: Vec<String>               = "[]",
179         /// Advanced option, fully override the command rust-analyzer uses for
180         /// formatting.
181         rustfmt_overrideCommand: Option<Vec<String>> = "null",
182     }
183 }
184
185 impl Default for ConfigData {
186     fn default() -> Self {
187         ConfigData::from_json(serde_json::Value::Null)
188     }
189 }
190
191 #[derive(Debug, Clone)]
192 pub struct Config {
193     caps: lsp_types::ClientCapabilities,
194     data: ConfigData,
195     pub discovered_projects: Option<Vec<ProjectManifest>>,
196     pub root_path: AbsPathBuf,
197 }
198
199 #[derive(Debug, Clone, Eq, PartialEq)]
200 pub enum LinkedProject {
201     ProjectManifest(ProjectManifest),
202     InlineJsonProject(ProjectJson),
203 }
204
205 impl From<ProjectManifest> for LinkedProject {
206     fn from(v: ProjectManifest) -> Self {
207         LinkedProject::ProjectManifest(v)
208     }
209 }
210
211 impl From<ProjectJson> for LinkedProject {
212     fn from(v: ProjectJson) -> Self {
213         LinkedProject::InlineJsonProject(v)
214     }
215 }
216
217 #[derive(Clone, Debug, PartialEq, Eq)]
218 pub struct LensConfig {
219     pub run: bool,
220     pub debug: bool,
221     pub implementations: bool,
222     pub method_refs: bool,
223 }
224
225 impl LensConfig {
226     pub fn any(&self) -> bool {
227         self.implementations || self.runnable() || self.references()
228     }
229
230     pub fn none(&self) -> bool {
231         !self.any()
232     }
233
234     pub fn runnable(&self) -> bool {
235         self.run || self.debug
236     }
237
238     pub fn references(&self) -> bool {
239         self.method_refs
240     }
241 }
242
243 #[derive(Debug, Clone)]
244 pub struct FilesConfig {
245     pub watcher: FilesWatcher,
246     pub exclude: Vec<String>,
247 }
248
249 #[derive(Debug, Clone)]
250 pub enum FilesWatcher {
251     Client,
252     Notify,
253 }
254
255 #[derive(Debug, Clone)]
256 pub struct NotificationsConfig {
257     pub cargo_toml_not_found: bool,
258 }
259
260 #[derive(Debug, Clone)]
261 pub enum RustfmtConfig {
262     Rustfmt { extra_args: Vec<String> },
263     CustomCommand { command: String, args: Vec<String> },
264 }
265
266 /// Configuration for runnable items, such as `main` function or tests.
267 #[derive(Debug, Clone)]
268 pub struct RunnablesConfig {
269     /// Custom command to be executed instead of `cargo` for runnables.
270     pub override_cargo: Option<String>,
271     /// Additional arguments for the `cargo`, e.g. `--release`.
272     pub cargo_extra_args: Vec<String>,
273 }
274
275 impl Config {
276     pub fn new(root_path: AbsPathBuf, caps: ClientCapabilities) -> Self {
277         Config { caps, data: ConfigData::default(), discovered_projects: None, root_path }
278     }
279     pub fn update(&mut self, json: serde_json::Value) {
280         log::info!("updating config from JSON: {:#}", json);
281         if json.is_null() || json.as_object().map_or(false, |it| it.is_empty()) {
282             return;
283         }
284         self.data = ConfigData::from_json(json);
285     }
286
287     pub fn json_schema() -> serde_json::Value {
288         ConfigData::json_schema()
289     }
290 }
291
292 macro_rules! try_ {
293     ($expr:expr) => {
294         || -> _ { Some($expr) }()
295     };
296 }
297 macro_rules! try_or {
298     ($expr:expr, $or:expr) => {
299         try_!($expr).unwrap_or($or)
300     };
301 }
302
303 impl Config {
304     pub fn linked_projects(&self) -> Vec<LinkedProject> {
305         if self.data.linkedProjects.is_empty() {
306             self.discovered_projects
307                 .as_ref()
308                 .into_iter()
309                 .flatten()
310                 .cloned()
311                 .map(LinkedProject::from)
312                 .collect()
313         } else {
314             self.data
315                 .linkedProjects
316                 .iter()
317                 .filter_map(|linked_project| {
318                     let res = match linked_project {
319                         ManifestOrProjectJson::Manifest(it) => {
320                             let path = self.root_path.join(it);
321                             ProjectManifest::from_manifest_file(path)
322                                 .map_err(|e| log::error!("failed to load linked project: {}", e))
323                                 .ok()?
324                                 .into()
325                         }
326                         ManifestOrProjectJson::ProjectJson(it) => {
327                             ProjectJson::new(&self.root_path, it.clone()).into()
328                         }
329                     };
330                     Some(res)
331                 })
332                 .collect()
333         }
334     }
335
336     pub fn location_link(&self) -> bool {
337         try_or!(self.caps.text_document.as_ref()?.definition?.link_support?, false)
338     }
339     pub fn line_folding_only(&self) -> bool {
340         try_or!(self.caps.text_document.as_ref()?.folding_range.as_ref()?.line_folding_only?, false)
341     }
342     pub fn hierarchical_symbols(&self) -> bool {
343         try_or!(
344             self.caps
345                 .text_document
346                 .as_ref()?
347                 .document_symbol
348                 .as_ref()?
349                 .hierarchical_document_symbol_support?,
350             false
351         )
352     }
353     pub fn code_action_literals(&self) -> bool {
354         try_!(self
355             .caps
356             .text_document
357             .as_ref()?
358             .code_action
359             .as_ref()?
360             .code_action_literal_support
361             .as_ref()?)
362         .is_some()
363     }
364     pub fn work_done_progress(&self) -> bool {
365         try_or!(self.caps.window.as_ref()?.work_done_progress?, false)
366     }
367     pub fn code_action_resolve(&self) -> bool {
368         try_or!(
369             self.caps
370                 .text_document
371                 .as_ref()?
372                 .code_action
373                 .as_ref()?
374                 .resolve_support
375                 .as_ref()?
376                 .properties
377                 .as_slice(),
378             &[]
379         )
380         .iter()
381         .any(|it| it == "edit")
382     }
383     pub fn signature_help_label_offsets(&self) -> bool {
384         try_or!(
385             self.caps
386                 .text_document
387                 .as_ref()?
388                 .signature_help
389                 .as_ref()?
390                 .signature_information
391                 .as_ref()?
392                 .parameter_information
393                 .as_ref()?
394                 .label_offset_support?,
395             false
396         )
397     }
398
399     fn experimental(&self, index: &'static str) -> bool {
400         try_or!(self.caps.experimental.as_ref()?.get(index)?.as_bool()?, false)
401     }
402     pub fn code_action_group(&self) -> bool {
403         self.experimental("codeActionGroup")
404     }
405     pub fn hover_actions(&self) -> bool {
406         self.experimental("hoverActions")
407     }
408     pub fn status_notification(&self) -> bool {
409         self.experimental("statusNotification")
410     }
411
412     pub fn publish_diagnostics(&self) -> bool {
413         self.data.diagnostics_enable
414     }
415     pub fn diagnostics(&self) -> DiagnosticsConfig {
416         DiagnosticsConfig {
417             disable_experimental: !self.data.diagnostics_enableExperimental,
418             disabled: self.data.diagnostics_disabled.clone(),
419         }
420     }
421     pub fn diagnostics_map(&self) -> DiagnosticsMapConfig {
422         DiagnosticsMapConfig {
423             warnings_as_info: self.data.diagnostics_warningsAsInfo.clone(),
424             warnings_as_hint: self.data.diagnostics_warningsAsHint.clone(),
425         }
426     }
427     pub fn lru_capacity(&self) -> Option<usize> {
428         self.data.lruCapacity
429     }
430     pub fn proc_macro_srv(&self) -> Option<(PathBuf, Vec<OsString>)> {
431         if !self.data.procMacro_enable {
432             return None;
433         }
434
435         let path = self.data.procMacro_server.clone().or_else(|| std::env::current_exe().ok())?;
436         Some((path, vec!["proc-macro".into()]))
437     }
438     pub fn files(&self) -> FilesConfig {
439         FilesConfig {
440             watcher: match self.data.files_watcher.as_str() {
441                 "notify" => FilesWatcher::Notify,
442                 "client" | _ => FilesWatcher::Client,
443             },
444             exclude: Vec::new(),
445         }
446     }
447     pub fn notifications(&self) -> NotificationsConfig {
448         NotificationsConfig { cargo_toml_not_found: self.data.notifications_cargoTomlNotFound }
449     }
450     pub fn cargo_autoreload(&self) -> bool {
451         self.data.cargo_autoreload
452     }
453     pub fn cargo(&self) -> CargoConfig {
454         let rustc_source = self.data.rustcSource.clone().and_then(|it| {
455             AbsPathBuf::try_from(it)
456                 .map_err(|_| log::error!("rustc source directory must be an absolute path"))
457                 .ok()
458         });
459
460         CargoConfig {
461             no_default_features: self.data.cargo_noDefaultFeatures,
462             all_features: self.data.cargo_allFeatures,
463             features: self.data.cargo_features.clone(),
464             load_out_dirs_from_check: self.data.cargo_loadOutDirsFromCheck,
465             target: self.data.cargo_target.clone(),
466             rustc_source,
467             no_sysroot: self.data.cargo_noSysroot,
468         }
469     }
470     pub fn rustfmt(&self) -> RustfmtConfig {
471         match &self.data.rustfmt_overrideCommand {
472             Some(args) if !args.is_empty() => {
473                 let mut args = args.clone();
474                 let command = args.remove(0);
475                 RustfmtConfig::CustomCommand { command, args }
476             }
477             Some(_) | None => {
478                 RustfmtConfig::Rustfmt { extra_args: self.data.rustfmt_extraArgs.clone() }
479             }
480         }
481     }
482     pub fn flycheck(&self) -> Option<FlycheckConfig> {
483         if !self.data.checkOnSave_enable {
484             return None;
485         }
486         let flycheck_config = match &self.data.checkOnSave_overrideCommand {
487             Some(args) if !args.is_empty() => {
488                 let mut args = args.clone();
489                 let command = args.remove(0);
490                 FlycheckConfig::CustomCommand { command, args }
491             }
492             Some(_) | None => FlycheckConfig::CargoCommand {
493                 command: self.data.checkOnSave_command.clone(),
494                 target_triple: self
495                     .data
496                     .checkOnSave_target
497                     .clone()
498                     .or(self.data.cargo_target.clone()),
499                 all_targets: self.data.checkOnSave_allTargets,
500                 no_default_features: self
501                     .data
502                     .checkOnSave_noDefaultFeatures
503                     .unwrap_or(self.data.cargo_noDefaultFeatures),
504                 all_features: self
505                     .data
506                     .checkOnSave_allFeatures
507                     .unwrap_or(self.data.cargo_allFeatures),
508                 features: self
509                     .data
510                     .checkOnSave_features
511                     .clone()
512                     .unwrap_or(self.data.cargo_features.clone()),
513                 extra_args: self.data.checkOnSave_extraArgs.clone(),
514             },
515         };
516         Some(flycheck_config)
517     }
518     pub fn runnables(&self) -> RunnablesConfig {
519         RunnablesConfig {
520             override_cargo: self.data.runnables_overrideCargo.clone(),
521             cargo_extra_args: self.data.runnables_cargoExtraArgs.clone(),
522         }
523     }
524     pub fn inlay_hints(&self) -> InlayHintsConfig {
525         InlayHintsConfig {
526             type_hints: self.data.inlayHints_typeHints,
527             parameter_hints: self.data.inlayHints_parameterHints,
528             chaining_hints: self.data.inlayHints_chainingHints,
529             max_length: self.data.inlayHints_maxLength,
530         }
531     }
532     fn merge_behavior(&self) -> Option<MergeBehavior> {
533         match self.data.assist_importMergeBehaviour {
534             MergeBehaviorDef::None => None,
535             MergeBehaviorDef::Full => Some(MergeBehavior::Full),
536             MergeBehaviorDef::Last => Some(MergeBehavior::Last),
537         }
538     }
539     pub fn completion(&self) -> CompletionConfig {
540         CompletionConfig {
541             enable_postfix_completions: self.data.completion_postfix_enable,
542             enable_autoimport_completions: self.data.completion_autoimport_enable
543                 && completion_item_edit_resolve(&self.caps),
544             add_call_parenthesis: self.data.completion_addCallParenthesis,
545             add_call_argument_snippets: self.data.completion_addCallArgumentSnippets,
546             merge: self.merge_behavior(),
547             snippet_cap: SnippetCap::new(try_or!(
548                 self.caps
549                     .text_document
550                     .as_ref()?
551                     .completion
552                     .as_ref()?
553                     .completion_item
554                     .as_ref()?
555                     .snippet_support?,
556                 false
557             )),
558         }
559     }
560     pub fn assist(&self) -> AssistConfig {
561         AssistConfig {
562             snippet_cap: SnippetCap::new(self.experimental("snippetTextEdit")),
563             allowed: None,
564             insert_use: InsertUseConfig {
565                 merge: self.merge_behavior(),
566                 prefix_kind: match self.data.assist_importPrefix {
567                     ImportPrefixDef::Plain => PrefixKind::Plain,
568                     ImportPrefixDef::ByCrate => PrefixKind::ByCrate,
569                     ImportPrefixDef::BySelf => PrefixKind::BySelf,
570                 },
571             },
572         }
573     }
574     pub fn call_info_full(&self) -> bool {
575         self.data.callInfo_full
576     }
577     pub fn lens(&self) -> LensConfig {
578         LensConfig {
579             run: self.data.lens_enable && self.data.lens_run,
580             debug: self.data.lens_enable && self.data.lens_debug,
581             implementations: self.data.lens_enable && self.data.lens_implementations,
582             method_refs: self.data.lens_enable && self.data.lens_methodReferences,
583         }
584     }
585     pub fn hover(&self) -> HoverConfig {
586         HoverConfig {
587             implementations: self.data.hoverActions_enable
588                 && self.data.hoverActions_implementations,
589             run: self.data.hoverActions_enable && self.data.hoverActions_run,
590             debug: self.data.hoverActions_enable && self.data.hoverActions_debug,
591             goto_type_def: self.data.hoverActions_enable && self.data.hoverActions_gotoTypeDef,
592             links_in_hover: self.data.hoverActions_linksInHover,
593             markdown: try_or!(
594                 self.caps
595                     .text_document
596                     .as_ref()?
597                     .hover
598                     .as_ref()?
599                     .content_format
600                     .as_ref()?
601                     .as_slice(),
602                 &[]
603             )
604             .contains(&MarkupKind::Markdown),
605         }
606     }
607     pub fn semantic_tokens_refresh(&self) -> bool {
608         try_or!(self.caps.workspace.as_ref()?.semantic_tokens.as_ref()?.refresh_support?, false)
609     }
610     pub fn code_lens_refresh(&self) -> bool {
611         try_or!(self.caps.workspace.as_ref()?.code_lens.as_ref()?.refresh_support?, false)
612     }
613 }
614
615 #[derive(Deserialize, Debug, Clone)]
616 #[serde(untagged)]
617 enum ManifestOrProjectJson {
618     Manifest(PathBuf),
619     ProjectJson(ProjectJsonData),
620 }
621
622 #[derive(Deserialize, Debug, Clone)]
623 #[serde(rename_all = "snake_case")]
624 enum MergeBehaviorDef {
625     None,
626     Full,
627     Last,
628 }
629
630 #[derive(Deserialize, Debug, Clone)]
631 #[serde(rename_all = "snake_case")]
632 enum ImportPrefixDef {
633     Plain,
634     BySelf,
635     ByCrate,
636 }
637
638 macro_rules! _config_data {
639     (struct $name:ident {
640         $(
641             $(#[doc=$doc:literal])*
642             $field:ident: $ty:ty = $default:expr,
643         )*
644     }) => {
645         #[allow(non_snake_case)]
646         #[derive(Debug, Clone)]
647         struct $name { $($field: $ty,)* }
648         impl $name {
649             fn from_json(mut json: serde_json::Value) -> $name {
650                 $name {$(
651                     $field: get_field(&mut json, stringify!($field), $default),
652                 )*}
653             }
654
655             fn json_schema() -> serde_json::Value {
656                 schema(&[
657                     $({
658                         let field = stringify!($field);
659                         let ty = stringify!($ty);
660                         (field, ty, &[$($doc),*], $default)
661                     },)*
662                 ])
663             }
664
665             #[cfg(test)]
666             fn manual() -> String {
667                 manual(&[
668                     $({
669                         let field = stringify!($field);
670                         let ty = stringify!($ty);
671                         (field, ty, &[$($doc),*], $default)
672                     },)*
673                 ])
674             }
675         }
676     };
677 }
678 use _config_data as config_data;
679
680 fn get_field<T: DeserializeOwned>(
681     json: &mut serde_json::Value,
682     field: &'static str,
683     default: &str,
684 ) -> T {
685     let default = serde_json::from_str(default).unwrap();
686
687     let mut pointer = field.replace('_', "/");
688     pointer.insert(0, '/');
689     json.pointer_mut(&pointer)
690         .and_then(|it| serde_json::from_value(it.take()).ok())
691         .unwrap_or(default)
692 }
693
694 fn schema(fields: &[(&'static str, &'static str, &[&str], &str)]) -> serde_json::Value {
695     for ((f1, ..), (f2, ..)) in fields.iter().zip(&fields[1..]) {
696         fn key(f: &str) -> &str {
697             f.splitn(2, "_").next().unwrap()
698         }
699         assert!(key(f1) <= key(f2), "wrong field order: {:?} {:?}", f1, f2);
700     }
701
702     let map = fields
703         .iter()
704         .map(|(field, ty, doc, default)| {
705             let name = field.replace("_", ".");
706             let name = format!("rust-analyzer.{}", name);
707             let props = field_props(field, ty, doc, default);
708             (name, props)
709         })
710         .collect::<serde_json::Map<_, _>>();
711     map.into()
712 }
713
714 fn field_props(field: &str, ty: &str, doc: &[&str], default: &str) -> serde_json::Value {
715     let doc = doc.iter().map(|it| it.trim()).join(" ");
716     assert!(
717         doc.ends_with('.') && doc.starts_with(char::is_uppercase),
718         "bad docs for {}: {:?}",
719         field,
720         doc
721     );
722     let default = default.parse::<serde_json::Value>().unwrap();
723
724     let mut map = serde_json::Map::default();
725     macro_rules! set {
726         ($($key:literal: $value:tt),*$(,)?) => {{$(
727             map.insert($key.into(), serde_json::json!($value));
728         )*}};
729     }
730     set!("markdownDescription": doc);
731     set!("default": default);
732
733     match ty {
734         "bool" => set!("type": "boolean"),
735         "String" => set!("type": "string"),
736         "Vec<String>" => set! {
737             "type": "array",
738             "items": { "type": "string" },
739         },
740         "FxHashSet<String>" => set! {
741             "type": "array",
742             "items": { "type": "string" },
743             "uniqueItems": true,
744         },
745         "Option<usize>" => set! {
746             "type": ["null", "integer"],
747             "minimum": 0,
748         },
749         "Option<String>" => set! {
750             "type": ["null", "string"],
751         },
752         "Option<PathBuf>" => set! {
753             "type": ["null", "string"],
754         },
755         "Option<bool>" => set! {
756             "type": ["null", "boolean"],
757         },
758         "Option<Vec<String>>" => set! {
759             "type": ["null", "array"],
760             "items": { "type": "string" },
761         },
762         "MergeBehaviorDef" => set! {
763             "type": "string",
764             "enum": ["none", "full", "last"],
765             "enumDescriptions": [
766                 "No merging",
767                 "Merge all layers of the import trees",
768                 "Only merge the last layer of the import trees"
769             ],
770         },
771         "ImportPrefixDef" => set! {
772             "type": "string",
773             "enum": [
774                 "plain",
775                 "by_self",
776                 "by_crate"
777             ],
778             "enumDescriptions": [
779                 "Insert import paths relative to the current module, using up to one `super` prefix if the parent module contains the requested item.",
780                 "Prefix all import paths with `self` if they don't begin with `self`, `super`, `crate` or a crate name.",
781                 "Force import paths to be absolute by always starting them with `crate` or the crate name they refer to."
782             ],
783         },
784         "Vec<ManifestOrProjectJson>" => set! {
785             "type": "array",
786             "items": { "type": ["string", "object"] },
787         },
788         _ => panic!("{}: {}", ty, default),
789     }
790
791     map.into()
792 }
793
794 #[cfg(test)]
795 fn manual(fields: &[(&'static str, &'static str, &[&str], &str)]) -> String {
796     fields
797         .iter()
798         .map(|(field, _ty, doc, default)| {
799             let name = format!("rust-analyzer.{}", field.replace("_", "."));
800             format!("[[{}]]{} (default: `{}`)::\n{}\n", name, name, default, doc.join(" "))
801         })
802         .collect::<String>()
803 }
804
805 #[cfg(test)]
806 mod tests {
807     use std::fs;
808
809     use test_utils::project_dir;
810
811     use super::*;
812
813     #[test]
814     fn schema_in_sync_with_package_json() {
815         let s = Config::json_schema();
816         let schema = format!("{:#}", s);
817         let schema = schema.trim_start_matches('{').trim_end_matches('}');
818
819         let package_json = project_dir().join("editors/code/package.json");
820         let package_json = fs::read_to_string(&package_json).unwrap();
821
822         let p = remove_ws(&package_json);
823         let s = remove_ws(&schema);
824
825         assert!(p.contains(&s), "update config in package.json. New config:\n{:#}", schema);
826     }
827
828     #[test]
829     fn schema_in_sync_with_docs() {
830         let docs_path = project_dir().join("docs/user/generated_config.adoc");
831         let current = fs::read_to_string(&docs_path).unwrap();
832         let expected = ConfigData::manual();
833
834         if remove_ws(&current) != remove_ws(&expected) {
835             fs::write(&docs_path, expected).unwrap();
836             panic!("updated config manual");
837         }
838     }
839
840     fn remove_ws(text: &str) -> String {
841         text.replace(char::is_whitespace, "")
842     }
843 }