language_settings.rs

   1//! Provides `language`-related settings.
   2
   3use crate::{File, Language, LanguageName, LanguageServerName};
   4use collections::{FxHashMap, HashMap, HashSet};
   5use ec4rs::{
   6    Properties as EditorconfigProperties,
   7    property::{FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs},
   8};
   9use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
  10use gpui::{App, Modifiers};
  11use itertools::{Either, Itertools};
  12use schemars::{JsonSchema, json_schema};
  13use serde::{Deserialize, Serialize};
  14
  15pub use settings::{
  16    CompletionSettings, EditPredictionProvider, EditPredictionsMode, FormatOnSave, Formatter,
  17    FormatterList, IndentGuideSettings, InlayHintKind, LanguageSettingsContent, LspInsertMode,
  18    RewrapBehavior, SelectedFormatter, ShowWhitespaceSetting, SoftWrap, WordsCompletionMode,
  19};
  20use settings::{
  21    ParameterizedJsonSchema, Settings, SettingsContent, SettingsLocation, SettingsStore, SettingsUi,
  22};
  23use shellexpand;
  24use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc};
  25use util::MergeFrom;
  26use util::{ResultExt, schemars::replace_subschema};
  27
  28/// Initializes the language settings.
  29pub fn init(cx: &mut App) {
  30    AllLanguageSettings::register(cx);
  31}
  32
  33/// Returns the settings for the specified language from the provided file.
  34pub fn language_settings<'a>(
  35    language: Option<LanguageName>,
  36    file: Option<&'a Arc<dyn File>>,
  37    cx: &'a App,
  38) -> Cow<'a, LanguageSettings> {
  39    let location = file.map(|f| SettingsLocation {
  40        worktree_id: f.worktree_id(cx),
  41        path: f.path().as_ref(),
  42    });
  43    AllLanguageSettings::get(location, cx).language(location, language.as_ref(), cx)
  44}
  45
  46/// Returns the settings for all languages from the provided file.
  47pub fn all_language_settings<'a>(
  48    file: Option<&'a Arc<dyn File>>,
  49    cx: &'a App,
  50) -> &'a AllLanguageSettings {
  51    let location = file.map(|f| SettingsLocation {
  52        worktree_id: f.worktree_id(cx),
  53        path: f.path().as_ref(),
  54    });
  55    AllLanguageSettings::get(location, cx)
  56}
  57
  58/// The settings for all languages.
  59#[derive(Debug, Clone)]
  60pub struct AllLanguageSettings {
  61    /// The edit prediction settings.
  62    pub edit_predictions: EditPredictionSettings,
  63    pub defaults: LanguageSettings,
  64    languages: HashMap<LanguageName, LanguageSettings>,
  65    pub(crate) file_types: FxHashMap<Arc<str>, GlobSet>,
  66    pub(crate) file_globs: FxHashMap<Arc<str>, Vec<String>>,
  67}
  68
  69/// The settings for a particular language.
  70#[derive(Debug, Clone)]
  71pub struct LanguageSettings {
  72    /// How many columns a tab should occupy.
  73    pub tab_size: NonZeroU32,
  74    /// Whether to indent lines using tab characters, as opposed to multiple
  75    /// spaces.
  76    pub hard_tabs: bool,
  77    /// How to soft-wrap long lines of text.
  78    pub soft_wrap: settings::SoftWrap,
  79    /// The column at which to soft-wrap lines, for buffers where soft-wrap
  80    /// is enabled.
  81    pub preferred_line_length: u32,
  82    /// Whether to show wrap guides (vertical rulers) in the editor.
  83    /// Setting this to true will show a guide at the 'preferred_line_length' value
  84    /// if softwrap is set to 'preferred_line_length', and will show any
  85    /// additional guides as specified by the 'wrap_guides' setting.
  86    pub show_wrap_guides: bool,
  87    /// Character counts at which to show wrap guides (vertical rulers) in the editor.
  88    pub wrap_guides: Vec<usize>,
  89    /// Indent guide related settings.
  90    /// todo!() shouldthis be not the content type?
  91    pub indent_guides: IndentGuideSettings,
  92    /// Whether or not to perform a buffer format before saving.
  93    pub format_on_save: FormatOnSave,
  94    /// Whether or not to remove any trailing whitespace from lines of a buffer
  95    /// before saving it.
  96    pub remove_trailing_whitespace_on_save: bool,
  97    /// Whether or not to ensure there's a single newline at the end of a buffer
  98    /// when saving it.
  99    pub ensure_final_newline_on_save: bool,
 100    /// How to perform a buffer format.
 101    pub formatter: settings::SelectedFormatter,
 102    /// Zed's Prettier integration settings.
 103    pub prettier: settings::PrettierSettingsContent,
 104    /// Whether to automatically close JSX tags.
 105    pub jsx_tag_auto_close: settings::JsxTagAutoCloseSettings,
 106    /// Whether to use language servers to provide code intelligence.
 107    pub enable_language_server: bool,
 108    /// The list of language servers to use (or disable) for this language.
 109    ///
 110    /// This array should consist of language server IDs, as well as the following
 111    /// special tokens:
 112    /// - `"!<language_server_id>"` - A language server ID prefixed with a `!` will be disabled.
 113    /// - `"..."` - A placeholder to refer to the **rest** of the registered language servers for this language.
 114    pub language_servers: Vec<String>,
 115    /// Controls where the `editor::Rewrap` action is allowed for this language.
 116    ///
 117    /// Note: This setting has no effect in Vim mode, as rewrap is already
 118    /// allowed everywhere.
 119    pub allow_rewrap: RewrapBehavior,
 120    /// Controls whether edit predictions are shown immediately (true)
 121    /// or manually by triggering `editor::ShowEditPrediction` (false).
 122    pub show_edit_predictions: bool,
 123    /// Controls whether edit predictions are shown in the given language
 124    /// scopes.
 125    pub edit_predictions_disabled_in: Vec<String>,
 126    /// Whether to show tabs and spaces in the editor.
 127    pub show_whitespaces: settings::ShowWhitespaceSetting,
 128    /// Visible characters used to render whitespace when show_whitespaces is enabled.
 129    pub whitespace_map: settings::WhitespaceMap,
 130    /// Whether to start a new line with a comment when a previous line is a comment as well.
 131    pub extend_comment_on_newline: bool,
 132    /// Inlay hint related settings.
 133    pub inlay_hints: InlayHintSettings,
 134    /// Whether to automatically close brackets.
 135    pub use_autoclose: bool,
 136    /// Whether to automatically surround text with brackets.
 137    pub use_auto_surround: bool,
 138    /// Whether to use additional LSP queries to format (and amend) the code after
 139    /// every "trigger" symbol input, defined by LSP server capabilities.
 140    pub use_on_type_format: bool,
 141    /// Whether indentation should be adjusted based on the context whilst typing.
 142    pub auto_indent: bool,
 143    /// Whether indentation of pasted content should be adjusted based on the context.
 144    pub auto_indent_on_paste: bool,
 145    /// Controls how the editor handles the autoclosed characters.
 146    pub always_treat_brackets_as_autoclosed: bool,
 147    /// Which code actions to run on save
 148    pub code_actions_on_format: HashMap<String, bool>,
 149    /// Whether to perform linked edits
 150    pub linked_edits: bool,
 151    /// Task configuration for this language.
 152    pub tasks: settings::LanguageTaskConfig,
 153    /// Whether to pop the completions menu while typing in an editor without
 154    /// explicitly requesting it.
 155    pub show_completions_on_input: bool,
 156    /// Whether to display inline and alongside documentation for items in the
 157    /// completions menu.
 158    pub show_completion_documentation: bool,
 159    /// Completion settings for this language.
 160    pub completions: settings::CompletionSettings,
 161    /// Preferred debuggers for this language.
 162    pub debuggers: Vec<String>,
 163}
 164
 165impl LanguageSettings {
 166    /// A token representing the rest of the available language servers.
 167    const REST_OF_LANGUAGE_SERVERS: &'static str = "...";
 168
 169    /// Returns the customized list of language servers from the list of
 170    /// available language servers.
 171    pub fn customized_language_servers(
 172        &self,
 173        available_language_servers: &[LanguageServerName],
 174    ) -> Vec<LanguageServerName> {
 175        Self::resolve_language_servers(&self.language_servers, available_language_servers)
 176    }
 177
 178    pub(crate) fn resolve_language_servers(
 179        configured_language_servers: &[String],
 180        available_language_servers: &[LanguageServerName],
 181    ) -> Vec<LanguageServerName> {
 182        let (disabled_language_servers, enabled_language_servers): (
 183            Vec<LanguageServerName>,
 184            Vec<LanguageServerName>,
 185        ) = configured_language_servers.iter().partition_map(
 186            |language_server| match language_server.strip_prefix('!') {
 187                Some(disabled) => Either::Left(LanguageServerName(disabled.to_string().into())),
 188                None => Either::Right(LanguageServerName(language_server.clone().into())),
 189            },
 190        );
 191
 192        let rest = available_language_servers
 193            .iter()
 194            .filter(|&available_language_server| {
 195                !disabled_language_servers.contains(available_language_server)
 196                    && !enabled_language_servers.contains(available_language_server)
 197            })
 198            .cloned()
 199            .collect::<Vec<_>>();
 200
 201        enabled_language_servers
 202            .into_iter()
 203            .flat_map(|language_server| {
 204                if language_server.0.as_ref() == Self::REST_OF_LANGUAGE_SERVERS {
 205                    rest.clone()
 206                } else {
 207                    vec![language_server]
 208                }
 209            })
 210            .collect::<Vec<_>>()
 211    }
 212}
 213
 214// The settings for inlay hints.
 215#[derive(Copy, Clone, Debug, PartialEq, Eq)]
 216pub struct InlayHintSettings {
 217    /// Global switch to toggle hints on and off.
 218    ///
 219    /// Default: false
 220    pub enabled: bool,
 221    /// Global switch to toggle inline values on and off when debugging.
 222    ///
 223    /// Default: true
 224    pub show_value_hints: bool,
 225    /// Whether type hints should be shown.
 226    ///
 227    /// Default: true
 228    pub show_type_hints: bool,
 229    /// Whether parameter hints should be shown.
 230    ///
 231    /// Default: true
 232    pub show_parameter_hints: bool,
 233    /// Whether other hints should be shown.
 234    ///
 235    /// Default: true
 236    pub show_other_hints: bool,
 237    /// Whether to show a background for inlay hints.
 238    ///
 239    /// If set to `true`, the background will use the `hint.background` color
 240    /// from the current theme.
 241    ///
 242    /// Default: false
 243    pub show_background: bool,
 244    /// Whether or not to debounce inlay hints updates after buffer edits.
 245    ///
 246    /// Set to 0 to disable debouncing.
 247    ///
 248    /// Default: 700
 249    pub edit_debounce_ms: u64,
 250    /// Whether or not to debounce inlay hints updates after buffer scrolls.
 251    ///
 252    /// Set to 0 to disable debouncing.
 253    ///
 254    /// Default: 50
 255    pub scroll_debounce_ms: u64,
 256    /// Toggles inlay hints (hides or shows) when the user presses the modifiers specified.
 257    /// If only a subset of the modifiers specified is pressed, hints are not toggled.
 258    /// If no modifiers are specified, this is equivalent to `None`.
 259    ///
 260    /// Default: None
 261    pub toggle_on_modifiers_press: Option<Modifiers>,
 262}
 263
 264impl InlayHintSettings {
 265    /// Returns the kinds of inlay hints that are enabled based on the settings.
 266    pub fn enabled_inlay_hint_kinds(&self) -> HashSet<Option<InlayHintKind>> {
 267        let mut kinds = HashSet::default();
 268        if self.show_type_hints {
 269            kinds.insert(Some(InlayHintKind::Type));
 270        }
 271        if self.show_parameter_hints {
 272            kinds.insert(Some(InlayHintKind::Parameter));
 273        }
 274        if self.show_other_hints {
 275            kinds.insert(None);
 276        }
 277        kinds
 278    }
 279}
 280
 281/// The settings for edit predictions, such as [GitHub Copilot](https://github.com/features/copilot)
 282/// or [Supermaven](https://supermaven.com).
 283#[derive(Clone, Debug, Default, SettingsUi)]
 284pub struct EditPredictionSettings {
 285    /// The provider that supplies edit predictions.
 286    pub provider: settings::EditPredictionProvider,
 287    /// A list of globs representing files that edit predictions should be disabled for.
 288    /// This list adds to a pre-existing, sensible default set of globs.
 289    /// Any additional ones you add are combined with them.
 290    #[settings_ui(skip)]
 291    pub disabled_globs: Vec<DisabledGlob>,
 292    /// Configures how edit predictions are displayed in the buffer.
 293    pub mode: settings::EditPredictionsMode,
 294    /// Settings specific to GitHub Copilot.
 295    pub copilot: CopilotSettings,
 296    /// Whether edit predictions are enabled in the assistant panel.
 297    /// This setting has no effect if globally disabled.
 298    pub enabled_in_text_threads: bool,
 299}
 300
 301impl EditPredictionSettings {
 302    /// Returns whether edit predictions are enabled for the given path.
 303    pub fn enabled_for_file(&self, file: &Arc<dyn File>, cx: &App) -> bool {
 304        !self.disabled_globs.iter().any(|glob| {
 305            if glob.is_absolute {
 306                file.as_local()
 307                    .is_some_and(|local| glob.matcher.is_match(local.abs_path(cx)))
 308            } else {
 309                glob.matcher.is_match(file.path())
 310            }
 311        })
 312    }
 313}
 314
 315#[derive(Clone, Debug)]
 316pub struct DisabledGlob {
 317    matcher: GlobMatcher,
 318    is_absolute: bool,
 319}
 320
 321#[derive(Clone, Debug, Default, SettingsUi)]
 322pub struct CopilotSettings {
 323    /// HTTP/HTTPS proxy to use for Copilot.
 324    #[settings_ui(skip)]
 325    pub proxy: Option<String>,
 326    /// Disable certificate verification for proxy (not recommended).
 327    pub proxy_no_verify: Option<bool>,
 328    /// Enterprise URI for Copilot.
 329    #[settings_ui(skip)]
 330    pub enterprise_uri: Option<String>,
 331}
 332
 333inventory::submit! {
 334    ParameterizedJsonSchema {
 335        add_and_get_ref: |generator, params, _cx| {
 336            let language_settings_content_ref = generator
 337                .subschema_for::<LanguageSettingsContent>()
 338                .to_value();
 339            replace_subschema::<settings::LanguageToSettingsMap>(generator, || json_schema!({
 340                "type": "object",
 341                "properties": params
 342                    .language_names
 343                    .iter()
 344                    .map(|name| {
 345                        (
 346                            name.clone(),
 347                            language_settings_content_ref.clone(),
 348                        )
 349                    })
 350                    .collect::<serde_json::Map<_, _>>()
 351            }))
 352        }
 353    }
 354}
 355
 356impl AllLanguageSettings {
 357    /// Returns the [`LanguageSettings`] for the language with the specified name.
 358    pub fn language<'a>(
 359        &'a self,
 360        location: Option<SettingsLocation<'a>>,
 361        language_name: Option<&LanguageName>,
 362        cx: &'a App,
 363    ) -> Cow<'a, LanguageSettings> {
 364        let settings = language_name
 365            .and_then(|name| self.languages.get(name))
 366            .unwrap_or(&self.defaults);
 367
 368        let editorconfig_properties = location.and_then(|location| {
 369            cx.global::<SettingsStore>()
 370                .editorconfig_properties(location.worktree_id, location.path)
 371        });
 372        if let Some(editorconfig_properties) = editorconfig_properties {
 373            let mut settings = settings.clone();
 374            merge_with_editorconfig(&mut settings, &editorconfig_properties);
 375            Cow::Owned(settings)
 376        } else {
 377            Cow::Borrowed(settings)
 378        }
 379    }
 380
 381    /// Returns whether edit predictions are enabled for the given path.
 382    pub fn edit_predictions_enabled_for_file(&self, file: &Arc<dyn File>, cx: &App) -> bool {
 383        self.edit_predictions.enabled_for_file(file, cx)
 384    }
 385
 386    /// Returns whether edit predictions are enabled for the given language and path.
 387    pub fn show_edit_predictions(&self, language: Option<&Arc<Language>>, cx: &App) -> bool {
 388        self.language(None, language.map(|l| l.name()).as_ref(), cx)
 389            .show_edit_predictions
 390    }
 391
 392    /// Returns the edit predictions preview mode for the given language and path.
 393    pub fn edit_predictions_mode(&self) -> EditPredictionsMode {
 394        self.edit_predictions.mode
 395    }
 396}
 397
 398fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) {
 399    let preferred_line_length = cfg.get::<MaxLineLen>().ok().and_then(|v| match v {
 400        MaxLineLen::Value(u) => Some(u as u32),
 401        MaxLineLen::Off => None,
 402    });
 403    let tab_size = cfg.get::<IndentSize>().ok().and_then(|v| match v {
 404        IndentSize::Value(u) => NonZeroU32::new(u as u32),
 405        IndentSize::UseTabWidth => cfg.get::<TabWidth>().ok().and_then(|w| match w {
 406            TabWidth::Value(u) => NonZeroU32::new(u as u32),
 407        }),
 408    });
 409    let hard_tabs = cfg
 410        .get::<IndentStyle>()
 411        .map(|v| v.eq(&IndentStyle::Tabs))
 412        .ok();
 413    let ensure_final_newline_on_save = cfg
 414        .get::<FinalNewline>()
 415        .map(|v| match v {
 416            FinalNewline::Value(b) => b,
 417        })
 418        .ok();
 419    let remove_trailing_whitespace_on_save = cfg
 420        .get::<TrimTrailingWs>()
 421        .map(|v| match v {
 422            TrimTrailingWs::Value(b) => b,
 423        })
 424        .ok();
 425    fn merge<T>(target: &mut T, value: Option<T>) {
 426        if let Some(value) = value {
 427            *target = value;
 428        }
 429    }
 430    merge(&mut settings.preferred_line_length, preferred_line_length);
 431    merge(&mut settings.tab_size, tab_size);
 432    merge(&mut settings.hard_tabs, hard_tabs);
 433    merge(
 434        &mut settings.remove_trailing_whitespace_on_save,
 435        remove_trailing_whitespace_on_save,
 436    );
 437    merge(
 438        &mut settings.ensure_final_newline_on_save,
 439        ensure_final_newline_on_save,
 440    );
 441}
 442
 443impl settings::Settings for AllLanguageSettings {
 444    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
 445        let all_languages = &content.project.all_languages;
 446        let defaults = all_languages.defaults.clone();
 447        let inlay_hints = defaults.inlay_hints.unwrap();
 448        let default_language_settings = LanguageSettings {
 449            tab_size: defaults.tab_size.unwrap(),
 450            hard_tabs: defaults.hard_tabs.unwrap(),
 451            soft_wrap: defaults.soft_wrap.unwrap(),
 452            preferred_line_length: defaults.preferred_line_length.unwrap(),
 453            show_wrap_guides: defaults.show_wrap_guides.unwrap(),
 454            wrap_guides: defaults.wrap_guides.unwrap(),
 455            indent_guides: defaults.indent_guides.unwrap(),
 456            format_on_save: defaults.format_on_save.unwrap(),
 457            remove_trailing_whitespace_on_save: defaults
 458                .remove_trailing_whitespace_on_save
 459                .unwrap(),
 460            ensure_final_newline_on_save: defaults.ensure_final_newline_on_save.unwrap(),
 461            formatter: defaults.formatter.unwrap(),
 462            prettier: defaults.prettier.unwrap(),
 463            jsx_tag_auto_close: defaults.jsx_tag_auto_close.unwrap(),
 464            enable_language_server: defaults.enable_language_server.unwrap(),
 465            language_servers: defaults.language_servers.unwrap(),
 466            allow_rewrap: defaults.allow_rewrap.unwrap(),
 467            show_edit_predictions: defaults.show_edit_predictions.unwrap(),
 468            edit_predictions_disabled_in: defaults.edit_predictions_disabled_in.unwrap(),
 469            show_whitespaces: defaults.show_whitespaces.unwrap(),
 470            whitespace_map: defaults.whitespace_map.unwrap(),
 471            extend_comment_on_newline: defaults.extend_comment_on_newline.unwrap(),
 472            inlay_hints: InlayHintSettings {
 473                enabled: inlay_hints.enabled.unwrap(),
 474                show_value_hints: inlay_hints.show_value_hints.unwrap(),
 475                show_type_hints: inlay_hints.show_type_hints.unwrap(),
 476                show_parameter_hints: inlay_hints.show_parameter_hints.unwrap(),
 477                show_other_hints: inlay_hints.show_other_hints.unwrap(),
 478                show_background: inlay_hints.show_background.unwrap(),
 479                edit_debounce_ms: inlay_hints.edit_debounce_ms.unwrap(),
 480                scroll_debounce_ms: inlay_hints.scroll_debounce_ms.unwrap(),
 481                toggle_on_modifiers_press: inlay_hints.toggle_on_modifiers_press,
 482            },
 483            use_autoclose: defaults.use_autoclose.unwrap(),
 484            use_auto_surround: defaults.use_auto_surround.unwrap(),
 485            use_on_type_format: defaults.use_on_type_format.unwrap(),
 486            auto_indent: defaults.auto_indent.unwrap(),
 487            auto_indent_on_paste: defaults.auto_indent_on_paste.unwrap(),
 488            always_treat_brackets_as_autoclosed: defaults
 489                .always_treat_brackets_as_autoclosed
 490                .unwrap(),
 491            code_actions_on_format: defaults.code_actions_on_format.unwrap(),
 492            linked_edits: defaults.linked_edits.unwrap(),
 493            tasks: defaults.tasks.unwrap(),
 494            show_completions_on_input: defaults.show_completions_on_input.unwrap(),
 495            show_completion_documentation: defaults.show_completion_documentation.unwrap(),
 496            completions: defaults.completions.unwrap(),
 497            debuggers: defaults.debuggers.unwrap(),
 498        };
 499
 500        let mut languages = HashMap::default();
 501        for (language_name, settings) in &all_languages.languages.0 {
 502            let mut language_settings = default_language_settings.clone();
 503            merge_settings(&mut language_settings, settings);
 504            languages.insert(LanguageName(language_name.clone()), language_settings);
 505        }
 506
 507        let edit_prediction_provider = all_languages
 508            .features
 509            .as_ref()
 510            .and_then(|f| f.edit_prediction_provider);
 511
 512        let edit_predictions = all_languages.edit_predictions.clone().unwrap();
 513        let edit_predictions_mode = edit_predictions.mode.unwrap();
 514
 515        let disabled_globs: HashSet<&String> = edit_predictions
 516            .disabled_globs
 517            .as_ref()
 518            .unwrap()
 519            .iter()
 520            .collect();
 521
 522        let copilot = edit_predictions.copilot.unwrap();
 523        let copilot_settings = CopilotSettings {
 524            proxy: copilot.proxy,
 525            proxy_no_verify: copilot.proxy_no_verify,
 526            enterprise_uri: copilot.enterprise_uri,
 527        };
 528
 529        let enabled_in_text_threads = edit_predictions.enabled_in_text_threads.unwrap();
 530
 531        let mut file_types: FxHashMap<Arc<str>, GlobSet> = FxHashMap::default();
 532        let mut file_globs: FxHashMap<Arc<str>, Vec<String>> = FxHashMap::default();
 533
 534        for (language, patterns) in &all_languages.file_types {
 535            let mut builder = GlobSetBuilder::new();
 536
 537            for pattern in patterns {
 538                builder.add(Glob::new(pattern).unwrap());
 539            }
 540
 541            file_types.insert(language.clone(), builder.build().unwrap());
 542            file_globs.insert(language.clone(), patterns.clone());
 543        }
 544
 545        Self {
 546            edit_predictions: EditPredictionSettings {
 547                provider: if let Some(provider) = edit_prediction_provider {
 548                    provider
 549                } else {
 550                    EditPredictionProvider::None
 551                },
 552                disabled_globs: disabled_globs
 553                    .iter()
 554                    .filter_map(|g| {
 555                        let expanded_g = shellexpand::tilde(g).into_owned();
 556                        Some(DisabledGlob {
 557                            matcher: globset::Glob::new(&expanded_g).ok()?.compile_matcher(),
 558                            is_absolute: Path::new(&expanded_g).is_absolute(),
 559                        })
 560                    })
 561                    .collect(),
 562                mode: edit_predictions_mode,
 563                copilot: copilot_settings,
 564                enabled_in_text_threads,
 565            },
 566            defaults: default_language_settings,
 567            languages,
 568            file_types,
 569            file_globs,
 570        }
 571    }
 572
 573    fn refine(&mut self, content: &SettingsContent, _cx: &mut App) {
 574        let all_languages = &content.project.all_languages;
 575        if let Some(provider) = all_languages
 576            .features
 577            .as_ref()
 578            .and_then(|f| f.edit_prediction_provider)
 579        {
 580            self.edit_predictions.provider = provider;
 581        }
 582
 583        if let Some(edit_predictions) = all_languages.edit_predictions.as_ref() {
 584            self.edit_predictions
 585                .mode
 586                .merge_from(&edit_predictions.mode);
 587            self.edit_predictions
 588                .enabled_in_text_threads
 589                .merge_from(&edit_predictions.enabled_in_text_threads);
 590
 591            if let Some(disabled_globs) = edit_predictions.disabled_globs.as_ref() {
 592                self.edit_predictions
 593                    .disabled_globs
 594                    .extend(disabled_globs.iter().filter_map(|g| {
 595                        let expanded_g = shellexpand::tilde(g).into_owned();
 596                        Some(DisabledGlob {
 597                            matcher: globset::Glob::new(&expanded_g).ok()?.compile_matcher(),
 598                            is_absolute: Path::new(&expanded_g).is_absolute(),
 599                        })
 600                    }));
 601            }
 602        }
 603
 604        if let Some(proxy) = all_languages
 605            .edit_predictions
 606            .as_ref()
 607            .and_then(|settings| settings.copilot.as_ref()?.proxy.clone())
 608        {
 609            self.edit_predictions.copilot.proxy = Some(proxy);
 610        }
 611
 612        if let Some(proxy_no_verify) = all_languages
 613            .edit_predictions
 614            .as_ref()
 615            .and_then(|settings| settings.copilot.as_ref()?.proxy_no_verify)
 616        {
 617            self.edit_predictions.copilot.proxy_no_verify = Some(proxy_no_verify);
 618        }
 619
 620        if let Some(enterprise_uri) = all_languages
 621            .edit_predictions
 622            .as_ref()
 623            .and_then(|settings| settings.copilot.as_ref()?.enterprise_uri.clone())
 624        {
 625            self.edit_predictions.copilot.enterprise_uri = Some(enterprise_uri);
 626        }
 627
 628        // A user's global settings override the default global settings and
 629        // all default language-specific settings.
 630        merge_settings(&mut self.defaults, &all_languages.defaults);
 631        for language_settings in self.languages.values_mut() {
 632            merge_settings(language_settings, &all_languages.defaults);
 633        }
 634
 635        // A user's language-specific settings override default language-specific settings.
 636        for (language_name, user_language_settings) in &all_languages.languages.0 {
 637            merge_settings(
 638                self.languages
 639                    .entry(LanguageName(language_name.clone()))
 640                    .or_insert_with(|| self.defaults.clone()),
 641                user_language_settings,
 642            );
 643        }
 644
 645        for (language, patterns) in &all_languages.file_types {
 646            let mut builder = GlobSetBuilder::new();
 647
 648            let default_value = self.file_globs.get(&language.clone());
 649
 650            // Merge the default value with the user's value.
 651            if let Some(patterns) = default_value {
 652                for pattern in patterns {
 653                    if let Some(glob) = Glob::new(pattern).log_err() {
 654                        builder.add(glob);
 655                    }
 656                }
 657            }
 658
 659            for pattern in patterns {
 660                if let Some(glob) = Glob::new(pattern).log_err() {
 661                    builder.add(glob);
 662                }
 663            }
 664
 665            self.file_globs
 666                .entry(language.clone())
 667                .or_default()
 668                .extend(patterns.clone());
 669
 670            if let Some(matcher) = builder.build().log_err() {
 671                self.file_types.insert(language.clone(), matcher);
 672            }
 673        }
 674    }
 675
 676    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
 677        let d = &mut current.project.all_languages.defaults;
 678        if let Some(size) = vscode
 679            .read_value("editor.tabSize")
 680            .and_then(|v| v.as_u64())
 681            .and_then(|n| NonZeroU32::new(n as u32))
 682        {
 683            d.tab_size = Some(size);
 684        }
 685        if let Some(v) = vscode.read_bool("editor.insertSpaces") {
 686            d.hard_tabs = Some(!v);
 687        }
 688
 689        vscode.enum_setting("editor.wordWrap", &mut d.soft_wrap, |s| match s {
 690            "on" => Some(SoftWrap::EditorWidth),
 691            "wordWrapColumn" => Some(SoftWrap::PreferLine),
 692            "bounded" => Some(SoftWrap::Bounded),
 693            "off" => Some(SoftWrap::None),
 694            _ => None,
 695        });
 696        vscode.u32_setting("editor.wordWrapColumn", &mut d.preferred_line_length);
 697
 698        if let Some(arr) = vscode
 699            .read_value("editor.rulers")
 700            .and_then(|v| v.as_array())
 701            .map(|v| v.iter().map(|n| n.as_u64().map(|n| n as usize)).collect())
 702        {
 703            d.wrap_guides = arr;
 704        }
 705        if let Some(b) = vscode.read_bool("editor.guides.indentation") {
 706            if let Some(guide_settings) = d.indent_guides.as_mut() {
 707                guide_settings.enabled = b;
 708            } else {
 709                d.indent_guides = Some(IndentGuideSettings {
 710                    enabled: b,
 711                    ..Default::default()
 712                });
 713            }
 714        }
 715
 716        if let Some(b) = vscode.read_bool("editor.guides.formatOnSave") {
 717            d.format_on_save = Some(if b {
 718                FormatOnSave::On
 719            } else {
 720                FormatOnSave::Off
 721            });
 722        }
 723        vscode.bool_setting(
 724            "editor.trimAutoWhitespace",
 725            &mut d.remove_trailing_whitespace_on_save,
 726        );
 727        vscode.bool_setting(
 728            "files.insertFinalNewline",
 729            &mut d.ensure_final_newline_on_save,
 730        );
 731        vscode.bool_setting("editor.inlineSuggest.enabled", &mut d.show_edit_predictions);
 732        vscode.enum_setting("editor.renderWhitespace", &mut d.show_whitespaces, |s| {
 733            Some(match s {
 734                "boundary" => ShowWhitespaceSetting::Boundary,
 735                "trailing" => ShowWhitespaceSetting::Trailing,
 736                "selection" => ShowWhitespaceSetting::Selection,
 737                "all" => ShowWhitespaceSetting::All,
 738                _ => ShowWhitespaceSetting::None,
 739            })
 740        });
 741        vscode.enum_setting(
 742            "editor.autoSurround",
 743            &mut d.use_auto_surround,
 744            |s| match s {
 745                "languageDefined" | "quotes" | "brackets" => Some(true),
 746                "never" => Some(false),
 747                _ => None,
 748            },
 749        );
 750        vscode.bool_setting("editor.formatOnType", &mut d.use_on_type_format);
 751        vscode.bool_setting("editor.linkedEditing", &mut d.linked_edits);
 752        vscode.bool_setting("editor.formatOnPaste", &mut d.auto_indent_on_paste);
 753        vscode.bool_setting(
 754            "editor.suggestOnTriggerCharacters",
 755            &mut d.show_completions_on_input,
 756        );
 757        if let Some(b) = vscode.read_bool("editor.suggest.showWords") {
 758            let mode = if b {
 759                WordsCompletionMode::Enabled
 760            } else {
 761                WordsCompletionMode::Disabled
 762            };
 763            if let Some(completion_settings) = d.completions.as_mut() {
 764                completion_settings.words = mode;
 765            } else {
 766                d.completions = Some(CompletionSettings {
 767                    words: mode,
 768                    words_min_length: 3,
 769                    lsp: true,
 770                    lsp_fetch_timeout_ms: 0,
 771                    lsp_insert_mode: LspInsertMode::ReplaceSuffix,
 772                });
 773            }
 774        }
 775        // TODO: pull ^ out into helper and reuse for per-language settings
 776
 777        // vscodes file association map is inverted from ours, so we flip the mapping before merging
 778        let mut associations: HashMap<Arc<str>, Vec<String>> = HashMap::default();
 779        if let Some(map) = vscode
 780            .read_value("files.associations")
 781            .and_then(|v| v.as_object())
 782        {
 783            for (k, v) in map {
 784                let Some(v) = v.as_str() else { continue };
 785                associations.entry(v.into()).or_default().push(k.clone());
 786            }
 787        }
 788
 789        // TODO: do we want to merge imported globs per filetype? for now we'll just replace
 790        current
 791            .project
 792            .all_languages
 793            .file_types
 794            .extend(associations);
 795
 796        // cursor global ignore list applies to cursor-tab, so transfer it to edit_predictions.disabled_globs
 797        if let Some(disabled_globs) = vscode
 798            .read_value("cursor.general.globalCursorIgnoreList")
 799            .and_then(|v| v.as_array())
 800        {
 801            current
 802                .project
 803                .all_languages
 804                .edit_predictions
 805                .get_or_insert_default()
 806                .disabled_globs
 807                .get_or_insert_default()
 808                .extend(
 809                    disabled_globs
 810                        .iter()
 811                        .filter_map(|glob| glob.as_str())
 812                        .map(|s| s.to_string()),
 813                );
 814        }
 815    }
 816}
 817
 818fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent) {
 819    settings.tab_size.merge_from(&src.tab_size);
 820    settings.tab_size = settings
 821        .tab_size
 822        .clamp(NonZeroU32::new(1).unwrap(), NonZeroU32::new(16).unwrap());
 823
 824    settings.hard_tabs.merge_from(&src.hard_tabs);
 825    settings.soft_wrap.merge_from(&src.soft_wrap);
 826    settings.use_autoclose.merge_from(&src.use_autoclose);
 827    settings
 828        .use_auto_surround
 829        .merge_from(&src.use_auto_surround);
 830    settings
 831        .use_on_type_format
 832        .merge_from(&src.use_on_type_format);
 833    settings.auto_indent.merge_from(&src.auto_indent);
 834    settings
 835        .auto_indent_on_paste
 836        .merge_from(&src.auto_indent_on_paste);
 837    settings
 838        .always_treat_brackets_as_autoclosed
 839        .merge_from(&src.always_treat_brackets_as_autoclosed);
 840    settings.show_wrap_guides.merge_from(&src.show_wrap_guides);
 841    settings.wrap_guides.merge_from(&src.wrap_guides);
 842    settings.indent_guides.merge_from(&src.indent_guides);
 843    settings
 844        .code_actions_on_format
 845        .merge_from(&src.code_actions_on_format.clone());
 846    settings.linked_edits.merge_from(&src.linked_edits);
 847    settings.tasks.merge_from(&src.tasks.clone());
 848
 849    settings
 850        .preferred_line_length
 851        .merge_from(&src.preferred_line_length);
 852    settings.formatter.merge_from(&src.formatter.clone());
 853    settings.prettier.merge_from(&src.prettier.clone());
 854    settings
 855        .jsx_tag_auto_close
 856        .merge_from(&src.jsx_tag_auto_close.clone());
 857    settings
 858        .format_on_save
 859        .merge_from(&src.format_on_save.clone());
 860    settings
 861        .remove_trailing_whitespace_on_save
 862        .merge_from(&src.remove_trailing_whitespace_on_save);
 863    settings
 864        .ensure_final_newline_on_save
 865        .merge_from(&src.ensure_final_newline_on_save);
 866    settings
 867        .enable_language_server
 868        .merge_from(&src.enable_language_server);
 869    settings
 870        .language_servers
 871        .merge_from(&src.language_servers.clone());
 872    settings.allow_rewrap.merge_from(&src.allow_rewrap);
 873    settings
 874        .show_edit_predictions
 875        .merge_from(&src.show_edit_predictions);
 876    settings
 877        .edit_predictions_disabled_in
 878        .merge_from(&src.edit_predictions_disabled_in.clone());
 879    settings.show_whitespaces.merge_from(&src.show_whitespaces);
 880    settings
 881        .whitespace_map
 882        .merge_from(&src.whitespace_map.clone());
 883    settings
 884        .extend_comment_on_newline
 885        .merge_from(&src.extend_comment_on_newline);
 886    if let Some(inlay_hints) = &src.inlay_hints {
 887        settings
 888            .inlay_hints
 889            .enabled
 890            .merge_from(&inlay_hints.enabled);
 891        settings
 892            .inlay_hints
 893            .show_value_hints
 894            .merge_from(&inlay_hints.show_value_hints);
 895        settings
 896            .inlay_hints
 897            .show_type_hints
 898            .merge_from(&inlay_hints.show_type_hints);
 899        settings
 900            .inlay_hints
 901            .show_parameter_hints
 902            .merge_from(&inlay_hints.show_parameter_hints);
 903        settings
 904            .inlay_hints
 905            .show_other_hints
 906            .merge_from(&inlay_hints.show_other_hints);
 907        settings
 908            .inlay_hints
 909            .show_background
 910            .merge_from(&inlay_hints.show_background);
 911        settings
 912            .inlay_hints
 913            .edit_debounce_ms
 914            .merge_from(&inlay_hints.edit_debounce_ms);
 915        settings
 916            .inlay_hints
 917            .scroll_debounce_ms
 918            .merge_from(&inlay_hints.scroll_debounce_ms);
 919        if let Some(toggle_on_modifiers_press) = &inlay_hints.toggle_on_modifiers_press {
 920            settings.inlay_hints.toggle_on_modifiers_press = Some(*toggle_on_modifiers_press);
 921        }
 922    }
 923    settings
 924        .show_completions_on_input
 925        .merge_from(&src.show_completions_on_input);
 926    settings
 927        .show_completion_documentation
 928        .merge_from(&src.show_completion_documentation);
 929    settings.completions.merge_from(&src.completions.clone());
 930}
 931
 932/// Allows to enable/disable formatting with Prettier
 933/// and configure default Prettier, used when no project-level Prettier installation is found.
 934/// Prettier formatting is disabled by default.
 935#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi)]
 936pub struct PrettierSettings {
 937    /// Enables or disables formatting with Prettier for a given language.
 938    #[serde(default)]
 939    pub allowed: bool,
 940
 941    /// Forces Prettier integration to use a specific parser name when formatting files with the language.
 942    #[serde(default)]
 943    pub parser: Option<String>,
 944
 945    /// Forces Prettier integration to use specific plugins when formatting files with the language.
 946    /// The default Prettier will be installed with these plugins.
 947    #[serde(default)]
 948    #[settings_ui(skip)]
 949    pub plugins: HashSet<String>,
 950
 951    /// Default Prettier options, in the format as in package.json section for Prettier.
 952    /// If project installs Prettier via its package.json, these options will be ignored.
 953    #[serde(flatten)]
 954    #[settings_ui(skip)]
 955    pub options: HashMap<String, serde_json::Value>,
 956}
 957
 958#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi)]
 959pub struct JsxTagAutoCloseSettings {
 960    /// Enables or disables auto-closing of JSX tags.
 961    #[serde(default)]
 962    pub enabled: bool,
 963}
 964
 965#[cfg(test)]
 966mod tests {
 967    use super::*;
 968    use gpui::TestAppContext;
 969
 970    #[gpui::test]
 971    fn test_edit_predictions_enabled_for_file(cx: &mut TestAppContext) {
 972        use crate::TestFile;
 973        use std::path::PathBuf;
 974
 975        let cx = cx.app.borrow_mut();
 976
 977        let build_settings = |globs: &[&str]| -> EditPredictionSettings {
 978            EditPredictionSettings {
 979                disabled_globs: globs
 980                    .iter()
 981                    .map(|glob_str| {
 982                        #[cfg(windows)]
 983                        let glob_str = {
 984                            let mut g = String::new();
 985
 986                            if glob_str.starts_with('/') {
 987                                g.push_str("C:");
 988                            }
 989
 990                            g.push_str(&glob_str.replace('/', "\\"));
 991                            g
 992                        };
 993                        #[cfg(windows)]
 994                        let glob_str = glob_str.as_str();
 995                        let expanded_glob_str = shellexpand::tilde(glob_str).into_owned();
 996                        DisabledGlob {
 997                            matcher: globset::Glob::new(&expanded_glob_str)
 998                                .unwrap()
 999                                .compile_matcher(),
1000                            is_absolute: Path::new(&expanded_glob_str).is_absolute(),
1001                        }
1002                    })
1003                    .collect(),
1004                ..Default::default()
1005            }
1006        };
1007
1008        const WORKTREE_NAME: &str = "project";
1009        let make_test_file = |segments: &[&str]| -> Arc<dyn File> {
1010            let mut path_buf = PathBuf::new();
1011            path_buf.extend(segments);
1012
1013            Arc::new(TestFile {
1014                path: path_buf.as_path().into(),
1015                root_name: WORKTREE_NAME.to_string(),
1016                local_root: Some(PathBuf::from(if cfg!(windows) {
1017                    "C:\\absolute\\"
1018                } else {
1019                    "/absolute/"
1020                })),
1021            })
1022        };
1023
1024        let test_file = make_test_file(&["src", "test", "file.rs"]);
1025
1026        // Test relative globs
1027        let settings = build_settings(&["*.rs"]);
1028        assert!(!settings.enabled_for_file(&test_file, &cx));
1029        let settings = build_settings(&["*.txt"]);
1030        assert!(settings.enabled_for_file(&test_file, &cx));
1031
1032        // Test absolute globs
1033        let settings = build_settings(&["/absolute/**/*.rs"]);
1034        assert!(!settings.enabled_for_file(&test_file, &cx));
1035        let settings = build_settings(&["/other/**/*.rs"]);
1036        assert!(settings.enabled_for_file(&test_file, &cx));
1037
1038        // Test exact path match relative
1039        let settings = build_settings(&["src/test/file.rs"]);
1040        assert!(!settings.enabled_for_file(&test_file, &cx));
1041        let settings = build_settings(&["src/test/otherfile.rs"]);
1042        assert!(settings.enabled_for_file(&test_file, &cx));
1043
1044        // Test exact path match absolute
1045        let settings = build_settings(&[&format!("/absolute/{}/src/test/file.rs", WORKTREE_NAME)]);
1046        assert!(!settings.enabled_for_file(&test_file, &cx));
1047        let settings = build_settings(&["/other/test/otherfile.rs"]);
1048        assert!(settings.enabled_for_file(&test_file, &cx));
1049
1050        // Test * glob
1051        let settings = build_settings(&["*"]);
1052        assert!(!settings.enabled_for_file(&test_file, &cx));
1053        let settings = build_settings(&["*.txt"]);
1054        assert!(settings.enabled_for_file(&test_file, &cx));
1055
1056        // Test **/* glob
1057        let settings = build_settings(&["**/*"]);
1058        assert!(!settings.enabled_for_file(&test_file, &cx));
1059        let settings = build_settings(&["other/**/*"]);
1060        assert!(settings.enabled_for_file(&test_file, &cx));
1061
1062        // Test directory/** glob
1063        let settings = build_settings(&["src/**"]);
1064        assert!(!settings.enabled_for_file(&test_file, &cx));
1065
1066        let test_file_root: Arc<dyn File> = Arc::new(TestFile {
1067            path: PathBuf::from("file.rs").as_path().into(),
1068            root_name: WORKTREE_NAME.to_string(),
1069            local_root: Some(PathBuf::from("/absolute/")),
1070        });
1071        assert!(settings.enabled_for_file(&test_file_root, &cx));
1072
1073        let settings = build_settings(&["other/**"]);
1074        assert!(settings.enabled_for_file(&test_file, &cx));
1075
1076        // Test **/directory/* glob
1077        let settings = build_settings(&["**/test/*"]);
1078        assert!(!settings.enabled_for_file(&test_file, &cx));
1079        let settings = build_settings(&["**/other/*"]);
1080        assert!(settings.enabled_for_file(&test_file, &cx));
1081
1082        // Test multiple globs
1083        let settings = build_settings(&["*.rs", "*.txt", "src/**"]);
1084        assert!(!settings.enabled_for_file(&test_file, &cx));
1085        let settings = build_settings(&["*.txt", "*.md", "other/**"]);
1086        assert!(settings.enabled_for_file(&test_file, &cx));
1087
1088        // Test dot files
1089        let dot_file = make_test_file(&[".config", "settings.json"]);
1090        let settings = build_settings(&[".*/**"]);
1091        assert!(!settings.enabled_for_file(&dot_file, &cx));
1092
1093        let dot_env_file = make_test_file(&[".env"]);
1094        let settings = build_settings(&[".env"]);
1095        assert!(!settings.enabled_for_file(&dot_env_file, &cx));
1096
1097        // Test tilde expansion
1098        let home = shellexpand::tilde("~").into_owned();
1099        let home_file = make_test_file(&[&home, "test.rs"]);
1100        let settings = build_settings(&["~/test.rs"]);
1101        assert!(!settings.enabled_for_file(&home_file, &cx));
1102    }
1103
1104    #[test]
1105    fn test_resolve_language_servers() {
1106        fn language_server_names(names: &[&str]) -> Vec<LanguageServerName> {
1107            names
1108                .iter()
1109                .copied()
1110                .map(|name| LanguageServerName(name.to_string().into()))
1111                .collect::<Vec<_>>()
1112        }
1113
1114        let available_language_servers = language_server_names(&[
1115            "typescript-language-server",
1116            "biome",
1117            "deno",
1118            "eslint",
1119            "tailwind",
1120        ]);
1121
1122        // A value of just `["..."]` is the same as taking all of the available language servers.
1123        assert_eq!(
1124            LanguageSettings::resolve_language_servers(
1125                &[LanguageSettings::REST_OF_LANGUAGE_SERVERS.into()],
1126                &available_language_servers,
1127            ),
1128            available_language_servers
1129        );
1130
1131        // Referencing one of the available language servers will change its order.
1132        assert_eq!(
1133            LanguageSettings::resolve_language_servers(
1134                &[
1135                    "biome".into(),
1136                    LanguageSettings::REST_OF_LANGUAGE_SERVERS.into(),
1137                    "deno".into()
1138                ],
1139                &available_language_servers
1140            ),
1141            language_server_names(&[
1142                "biome",
1143                "typescript-language-server",
1144                "eslint",
1145                "tailwind",
1146                "deno",
1147            ])
1148        );
1149
1150        // Negating an available language server removes it from the list.
1151        assert_eq!(
1152            LanguageSettings::resolve_language_servers(
1153                &[
1154                    "deno".into(),
1155                    "!typescript-language-server".into(),
1156                    "!biome".into(),
1157                    LanguageSettings::REST_OF_LANGUAGE_SERVERS.into()
1158                ],
1159                &available_language_servers
1160            ),
1161            language_server_names(&["deno", "eslint", "tailwind"])
1162        );
1163
1164        // Adding a language server not in the list of available language servers adds it to the list.
1165        assert_eq!(
1166            LanguageSettings::resolve_language_servers(
1167                &[
1168                    "my-cool-language-server".into(),
1169                    LanguageSettings::REST_OF_LANGUAGE_SERVERS.into()
1170                ],
1171                &available_language_servers
1172            ),
1173            language_server_names(&[
1174                "my-cool-language-server",
1175                "typescript-language-server",
1176                "biome",
1177                "deno",
1178                "eslint",
1179                "tailwind",
1180            ])
1181        );
1182    }
1183}