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;
  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: settings::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 edit predictions, such as [GitHub Copilot](https://github.com/features/copilot)
 215/// or [Supermaven](https://supermaven.com).
 216#[derive(Clone, Debug, Default, SettingsUi)]
 217pub struct EditPredictionSettings {
 218    /// The provider that supplies edit predictions.
 219    pub provider: settings::EditPredictionProvider,
 220    /// A list of globs representing files that edit predictions should be disabled for.
 221    /// This list adds to a pre-existing, sensible default set of globs.
 222    /// Any additional ones you add are combined with them.
 223    #[settings_ui(skip)]
 224    pub disabled_globs: Vec<DisabledGlob>,
 225    /// Configures how edit predictions are displayed in the buffer.
 226    pub mode: settings::EditPredictionsMode,
 227    /// Settings specific to GitHub Copilot.
 228    pub copilot: CopilotSettings,
 229    /// Whether edit predictions are enabled in the assistant panel.
 230    /// This setting has no effect if globally disabled.
 231    pub enabled_in_text_threads: bool,
 232}
 233
 234impl EditPredictionSettings {
 235    /// Returns whether edit predictions are enabled for the given path.
 236    pub fn enabled_for_file(&self, file: &Arc<dyn File>, cx: &App) -> bool {
 237        !self.disabled_globs.iter().any(|glob| {
 238            if glob.is_absolute {
 239                file.as_local()
 240                    .is_some_and(|local| glob.matcher.is_match(local.abs_path(cx)))
 241            } else {
 242                glob.matcher.is_match(file.path())
 243            }
 244        })
 245    }
 246}
 247
 248#[derive(Clone, Debug)]
 249pub struct DisabledGlob {
 250    matcher: GlobMatcher,
 251    is_absolute: bool,
 252}
 253
 254#[derive(Clone, Debug, Default, SettingsUi)]
 255pub struct CopilotSettings {
 256    /// HTTP/HTTPS proxy to use for Copilot.
 257    #[settings_ui(skip)]
 258    pub proxy: Option<String>,
 259    /// Disable certificate verification for proxy (not recommended).
 260    pub proxy_no_verify: Option<bool>,
 261    /// Enterprise URI for Copilot.
 262    #[settings_ui(skip)]
 263    pub enterprise_uri: Option<String>,
 264}
 265
 266inventory::submit! {
 267    ParameterizedJsonSchema {
 268        add_and_get_ref: |generator, params, _cx| {
 269            let language_settings_content_ref = generator
 270                .subschema_for::<LanguageSettingsContent>()
 271                .to_value();
 272            replace_subschema::<settings::LanguageToSettingsMap>(generator, || json_schema!({
 273                "type": "object",
 274                "properties": params
 275                    .language_names
 276                    .iter()
 277                    .map(|name| {
 278                        (
 279                            name.clone(),
 280                            language_settings_content_ref.clone(),
 281                        )
 282                    })
 283                    .collect::<serde_json::Map<_, _>>()
 284            }))
 285        }
 286    }
 287}
 288
 289impl AllLanguageSettings {
 290    /// Returns the [`LanguageSettings`] for the language with the specified name.
 291    pub fn language<'a>(
 292        &'a self,
 293        location: Option<SettingsLocation<'a>>,
 294        language_name: Option<&LanguageName>,
 295        cx: &'a App,
 296    ) -> Cow<'a, LanguageSettings> {
 297        let settings = language_name
 298            .and_then(|name| self.languages.get(name))
 299            .unwrap_or(&self.defaults);
 300
 301        let editorconfig_properties = location.and_then(|location| {
 302            cx.global::<SettingsStore>()
 303                .editorconfig_properties(location.worktree_id, location.path)
 304        });
 305        if let Some(editorconfig_properties) = editorconfig_properties {
 306            let mut settings = settings.clone();
 307            merge_with_editorconfig(&mut settings, &editorconfig_properties);
 308            Cow::Owned(settings)
 309        } else {
 310            Cow::Borrowed(settings)
 311        }
 312    }
 313
 314    /// Returns whether edit predictions are enabled for the given path.
 315    pub fn edit_predictions_enabled_for_file(&self, file: &Arc<dyn File>, cx: &App) -> bool {
 316        self.edit_predictions.enabled_for_file(file, cx)
 317    }
 318
 319    /// Returns whether edit predictions are enabled for the given language and path.
 320    pub fn show_edit_predictions(&self, language: Option<&Arc<Language>>, cx: &App) -> bool {
 321        self.language(None, language.map(|l| l.name()).as_ref(), cx)
 322            .show_edit_predictions
 323    }
 324
 325    /// Returns the edit predictions preview mode for the given language and path.
 326    pub fn edit_predictions_mode(&self) -> EditPredictionsMode {
 327        self.edit_predictions.mode
 328    }
 329}
 330
 331fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) {
 332    let preferred_line_length = cfg.get::<MaxLineLen>().ok().and_then(|v| match v {
 333        MaxLineLen::Value(u) => Some(u as u32),
 334        MaxLineLen::Off => None,
 335    });
 336    let tab_size = cfg.get::<IndentSize>().ok().and_then(|v| match v {
 337        IndentSize::Value(u) => NonZeroU32::new(u as u32),
 338        IndentSize::UseTabWidth => cfg.get::<TabWidth>().ok().and_then(|w| match w {
 339            TabWidth::Value(u) => NonZeroU32::new(u as u32),
 340        }),
 341    });
 342    let hard_tabs = cfg
 343        .get::<IndentStyle>()
 344        .map(|v| v.eq(&IndentStyle::Tabs))
 345        .ok();
 346    let ensure_final_newline_on_save = cfg
 347        .get::<FinalNewline>()
 348        .map(|v| match v {
 349            FinalNewline::Value(b) => b,
 350        })
 351        .ok();
 352    let remove_trailing_whitespace_on_save = cfg
 353        .get::<TrimTrailingWs>()
 354        .map(|v| match v {
 355            TrimTrailingWs::Value(b) => b,
 356        })
 357        .ok();
 358    fn merge<T>(target: &mut T, value: Option<T>) {
 359        if let Some(value) = value {
 360            *target = value;
 361        }
 362    }
 363    merge(&mut settings.preferred_line_length, preferred_line_length);
 364    merge(&mut settings.tab_size, tab_size);
 365    merge(&mut settings.hard_tabs, hard_tabs);
 366    merge(
 367        &mut settings.remove_trailing_whitespace_on_save,
 368        remove_trailing_whitespace_on_save,
 369    );
 370    merge(
 371        &mut settings.ensure_final_newline_on_save,
 372        ensure_final_newline_on_save,
 373    );
 374}
 375
 376impl settings::Settings for AllLanguageSettings {
 377    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
 378        let all_languages = &content.project.all_languages;
 379        let defaults = all_languages.defaults.clone();
 380        let default_language_settings = LanguageSettings {
 381            tab_size: defaults.tab_size.unwrap(),
 382            hard_tabs: defaults.hard_tabs.unwrap(),
 383            soft_wrap: defaults.soft_wrap.unwrap(),
 384            preferred_line_length: defaults.preferred_line_length.unwrap(),
 385            show_wrap_guides: defaults.show_wrap_guides.unwrap(),
 386            wrap_guides: defaults.wrap_guides.unwrap(),
 387            indent_guides: defaults.indent_guides.unwrap(),
 388            format_on_save: defaults.format_on_save.unwrap(),
 389            remove_trailing_whitespace_on_save: defaults
 390                .remove_trailing_whitespace_on_save
 391                .unwrap(),
 392            ensure_final_newline_on_save: defaults.ensure_final_newline_on_save.unwrap(),
 393            formatter: defaults.formatter.unwrap(),
 394            prettier: defaults.prettier.unwrap(),
 395            jsx_tag_auto_close: defaults.jsx_tag_auto_close.unwrap(),
 396            enable_language_server: defaults.enable_language_server.unwrap(),
 397            language_servers: defaults.language_servers.unwrap(),
 398            allow_rewrap: defaults.allow_rewrap.unwrap(),
 399            show_edit_predictions: defaults.show_edit_predictions.unwrap(),
 400            edit_predictions_disabled_in: defaults.edit_predictions_disabled_in.unwrap(),
 401            show_whitespaces: defaults.show_whitespaces.unwrap(),
 402            whitespace_map: defaults.whitespace_map.unwrap(),
 403            extend_comment_on_newline: defaults.extend_comment_on_newline.unwrap(),
 404            inlay_hints: defaults.inlay_hints.unwrap(),
 405            use_autoclose: defaults.use_autoclose.unwrap(),
 406            use_auto_surround: defaults.use_auto_surround.unwrap(),
 407            use_on_type_format: defaults.use_on_type_format.unwrap(),
 408            auto_indent: defaults.auto_indent.unwrap(),
 409            auto_indent_on_paste: defaults.auto_indent_on_paste.unwrap(),
 410            always_treat_brackets_as_autoclosed: defaults
 411                .always_treat_brackets_as_autoclosed
 412                .unwrap(),
 413            code_actions_on_format: defaults.code_actions_on_format.unwrap(),
 414            linked_edits: defaults.linked_edits.unwrap(),
 415            tasks: defaults.tasks.unwrap(),
 416            show_completions_on_input: defaults.show_completions_on_input.unwrap(),
 417            show_completion_documentation: defaults.show_completion_documentation.unwrap(),
 418            completions: defaults.completions.unwrap(),
 419            debuggers: defaults.debuggers.unwrap(),
 420        };
 421
 422        let mut languages = HashMap::default();
 423        for (language_name, settings) in &all_languages.languages.0 {
 424            let mut language_settings = default_language_settings.clone();
 425            merge_settings(&mut language_settings, settings);
 426            languages.insert(LanguageName(language_name.clone()), language_settings);
 427        }
 428
 429        let edit_prediction_provider = all_languages
 430            .features
 431            .as_ref()
 432            .and_then(|f| f.edit_prediction_provider);
 433
 434        let edit_predictions = all_languages.edit_predictions.clone().unwrap();
 435        let edit_predictions_mode = edit_predictions.mode.unwrap();
 436
 437        let disabled_globs: HashSet<&String> = edit_predictions
 438            .disabled_globs
 439            .as_ref()
 440            .unwrap()
 441            .iter()
 442            .collect();
 443
 444        let copilot = edit_predictions.copilot.unwrap();
 445        let copilot_settings = CopilotSettings {
 446            proxy: copilot.proxy,
 447            proxy_no_verify: copilot.proxy_no_verify,
 448            enterprise_uri: copilot.enterprise_uri,
 449        };
 450
 451        let enabled_in_text_threads = edit_predictions.enabled_in_text_threads.unwrap();
 452
 453        let mut file_types: FxHashMap<Arc<str>, GlobSet> = FxHashMap::default();
 454        let mut file_globs: FxHashMap<Arc<str>, Vec<String>> = FxHashMap::default();
 455
 456        for (language, patterns) in &all_languages.file_types {
 457            let mut builder = GlobSetBuilder::new();
 458
 459            for pattern in patterns {
 460                builder.add(Glob::new(pattern).unwrap());
 461            }
 462
 463            file_types.insert(language.clone(), builder.build().unwrap());
 464            file_globs.insert(language.clone(), patterns.clone());
 465        }
 466
 467        Self {
 468            edit_predictions: EditPredictionSettings {
 469                provider: if let Some(provider) = edit_prediction_provider {
 470                    provider
 471                } else {
 472                    EditPredictionProvider::None
 473                },
 474                disabled_globs: disabled_globs
 475                    .iter()
 476                    .filter_map(|g| {
 477                        let expanded_g = shellexpand::tilde(g).into_owned();
 478                        Some(DisabledGlob {
 479                            matcher: globset::Glob::new(&expanded_g).ok()?.compile_matcher(),
 480                            is_absolute: Path::new(&expanded_g).is_absolute(),
 481                        })
 482                    })
 483                    .collect(),
 484                mode: edit_predictions_mode,
 485                copilot: copilot_settings,
 486                enabled_in_text_threads,
 487            },
 488            defaults: default_language_settings,
 489            languages,
 490            file_types,
 491            file_globs,
 492        }
 493    }
 494
 495    fn refine(&mut self, content: &SettingsContent, _cx: &mut App) {
 496        let all_languages = &content.project.all_languages;
 497        if let Some(provider) = all_languages
 498            .features
 499            .as_ref()
 500            .and_then(|f| f.edit_prediction_provider)
 501        {
 502            self.edit_predictions.provider = provider;
 503        }
 504
 505        if let Some(edit_predictions) = all_languages.edit_predictions.as_ref() {
 506            self.edit_predictions
 507                .mode
 508                .merge_from(&edit_predictions.mode);
 509            self.edit_predictions
 510                .enabled_in_text_threads
 511                .merge_from(&edit_predictions.enabled_in_text_threads);
 512
 513            if let Some(disabled_globs) = edit_predictions.disabled_globs.as_ref() {
 514                self.edit_predictions
 515                    .disabled_globs
 516                    .extend(disabled_globs.iter().filter_map(|g| {
 517                        let expanded_g = shellexpand::tilde(g).into_owned();
 518                        Some(DisabledGlob {
 519                            matcher: globset::Glob::new(&expanded_g).ok()?.compile_matcher(),
 520                            is_absolute: Path::new(&expanded_g).is_absolute(),
 521                        })
 522                    }));
 523            }
 524        }
 525
 526        if let Some(proxy) = all_languages
 527            .edit_predictions
 528            .as_ref()
 529            .and_then(|settings| settings.copilot.as_ref()?.proxy.clone())
 530        {
 531            self.edit_predictions.copilot.proxy = Some(proxy);
 532        }
 533
 534        if let Some(proxy_no_verify) = all_languages
 535            .edit_predictions
 536            .as_ref()
 537            .and_then(|settings| settings.copilot.as_ref()?.proxy_no_verify)
 538        {
 539            self.edit_predictions.copilot.proxy_no_verify = Some(proxy_no_verify);
 540        }
 541
 542        if let Some(enterprise_uri) = all_languages
 543            .edit_predictions
 544            .as_ref()
 545            .and_then(|settings| settings.copilot.as_ref()?.enterprise_uri.clone())
 546        {
 547            self.edit_predictions.copilot.enterprise_uri = Some(enterprise_uri);
 548        }
 549
 550        // A user's global settings override the default global settings and
 551        // all default language-specific settings.
 552        merge_settings(&mut self.defaults, &all_languages.defaults);
 553        for language_settings in self.languages.values_mut() {
 554            merge_settings(language_settings, &all_languages.defaults);
 555        }
 556
 557        // A user's language-specific settings override default language-specific settings.
 558        for (language_name, user_language_settings) in &all_languages.languages.0 {
 559            merge_settings(
 560                self.languages
 561                    .entry(LanguageName(language_name.clone()))
 562                    .or_insert_with(|| self.defaults.clone()),
 563                user_language_settings,
 564            );
 565        }
 566
 567        for (language, patterns) in &all_languages.file_types {
 568            let mut builder = GlobSetBuilder::new();
 569
 570            let default_value = self.file_globs.get(&language.clone());
 571
 572            // Merge the default value with the user's value.
 573            if let Some(patterns) = default_value {
 574                for pattern in patterns {
 575                    if let Some(glob) = Glob::new(pattern).log_err() {
 576                        builder.add(glob);
 577                    }
 578                }
 579            }
 580
 581            for pattern in patterns {
 582                if let Some(glob) = Glob::new(pattern).log_err() {
 583                    builder.add(glob);
 584                }
 585            }
 586
 587            self.file_globs
 588                .entry(language.clone())
 589                .or_default()
 590                .extend(patterns.clone());
 591
 592            if let Some(matcher) = builder.build().log_err() {
 593                self.file_types.insert(language.clone(), matcher);
 594            }
 595        }
 596    }
 597
 598    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
 599        let d = &mut current.project.all_languages.defaults;
 600        if let Some(size) = vscode
 601            .read_value("editor.tabSize")
 602            .and_then(|v| v.as_u64())
 603            .and_then(|n| NonZeroU32::new(n as u32))
 604        {
 605            d.tab_size = Some(size);
 606        }
 607        if let Some(v) = vscode.read_bool("editor.insertSpaces") {
 608            d.hard_tabs = Some(!v);
 609        }
 610
 611        vscode.enum_setting("editor.wordWrap", &mut d.soft_wrap, |s| match s {
 612            "on" => Some(SoftWrap::EditorWidth),
 613            "wordWrapColumn" => Some(SoftWrap::PreferLine),
 614            "bounded" => Some(SoftWrap::Bounded),
 615            "off" => Some(SoftWrap::None),
 616            _ => None,
 617        });
 618        vscode.u32_setting("editor.wordWrapColumn", &mut d.preferred_line_length);
 619
 620        if let Some(arr) = vscode
 621            .read_value("editor.rulers")
 622            .and_then(|v| v.as_array())
 623            .map(|v| v.iter().map(|n| n.as_u64().map(|n| n as usize)).collect())
 624        {
 625            d.wrap_guides = arr;
 626        }
 627        if let Some(b) = vscode.read_bool("editor.guides.indentation") {
 628            if let Some(guide_settings) = d.indent_guides.as_mut() {
 629                guide_settings.enabled = b;
 630            } else {
 631                d.indent_guides = Some(IndentGuideSettings {
 632                    enabled: b,
 633                    ..Default::default()
 634                });
 635            }
 636        }
 637
 638        if let Some(b) = vscode.read_bool("editor.guides.formatOnSave") {
 639            d.format_on_save = Some(if b {
 640                FormatOnSave::On
 641            } else {
 642                FormatOnSave::Off
 643            });
 644        }
 645        vscode.bool_setting(
 646            "editor.trimAutoWhitespace",
 647            &mut d.remove_trailing_whitespace_on_save,
 648        );
 649        vscode.bool_setting(
 650            "files.insertFinalNewline",
 651            &mut d.ensure_final_newline_on_save,
 652        );
 653        vscode.bool_setting("editor.inlineSuggest.enabled", &mut d.show_edit_predictions);
 654        vscode.enum_setting("editor.renderWhitespace", &mut d.show_whitespaces, |s| {
 655            Some(match s {
 656                "boundary" => ShowWhitespaceSetting::Boundary,
 657                "trailing" => ShowWhitespaceSetting::Trailing,
 658                "selection" => ShowWhitespaceSetting::Selection,
 659                "all" => ShowWhitespaceSetting::All,
 660                _ => ShowWhitespaceSetting::None,
 661            })
 662        });
 663        vscode.enum_setting(
 664            "editor.autoSurround",
 665            &mut d.use_auto_surround,
 666            |s| match s {
 667                "languageDefined" | "quotes" | "brackets" => Some(true),
 668                "never" => Some(false),
 669                _ => None,
 670            },
 671        );
 672        vscode.bool_setting("editor.formatOnType", &mut d.use_on_type_format);
 673        vscode.bool_setting("editor.linkedEditing", &mut d.linked_edits);
 674        vscode.bool_setting("editor.formatOnPaste", &mut d.auto_indent_on_paste);
 675        vscode.bool_setting(
 676            "editor.suggestOnTriggerCharacters",
 677            &mut d.show_completions_on_input,
 678        );
 679        if let Some(b) = vscode.read_bool("editor.suggest.showWords") {
 680            let mode = if b {
 681                WordsCompletionMode::Enabled
 682            } else {
 683                WordsCompletionMode::Disabled
 684            };
 685            if let Some(completion_settings) = d.completions.as_mut() {
 686                completion_settings.words = mode;
 687            } else {
 688                d.completions = Some(CompletionSettings {
 689                    words: mode,
 690                    words_min_length: 3,
 691                    lsp: true,
 692                    lsp_fetch_timeout_ms: 0,
 693                    lsp_insert_mode: LspInsertMode::ReplaceSuffix,
 694                });
 695            }
 696        }
 697        // TODO: pull ^ out into helper and reuse for per-language settings
 698
 699        // vscodes file association map is inverted from ours, so we flip the mapping before merging
 700        let mut associations: HashMap<Arc<str>, Vec<String>> = HashMap::default();
 701        if let Some(map) = vscode
 702            .read_value("files.associations")
 703            .and_then(|v| v.as_object())
 704        {
 705            for (k, v) in map {
 706                let Some(v) = v.as_str() else { continue };
 707                associations.entry(v.into()).or_default().push(k.clone());
 708            }
 709        }
 710
 711        // TODO: do we want to merge imported globs per filetype? for now we'll just replace
 712        current
 713            .project
 714            .all_languages
 715            .file_types
 716            .extend(associations);
 717
 718        // cursor global ignore list applies to cursor-tab, so transfer it to edit_predictions.disabled_globs
 719        if let Some(disabled_globs) = vscode
 720            .read_value("cursor.general.globalCursorIgnoreList")
 721            .and_then(|v| v.as_array())
 722        {
 723            current
 724                .project
 725                .all_languages
 726                .edit_predictions
 727                .get_or_insert_default()
 728                .disabled_globs
 729                .get_or_insert_default()
 730                .extend(
 731                    disabled_globs
 732                        .iter()
 733                        .filter_map(|glob| glob.as_str())
 734                        .map(|s| s.to_string()),
 735                );
 736        }
 737    }
 738}
 739
 740fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent) {
 741    settings.tab_size.merge_from(&src.tab_size);
 742    settings.tab_size = settings
 743        .tab_size
 744        .clamp(NonZeroU32::new(1).unwrap(), NonZeroU32::new(16).unwrap());
 745
 746    settings.hard_tabs.merge_from(&src.hard_tabs);
 747    settings.soft_wrap.merge_from(&src.soft_wrap);
 748    settings.use_autoclose.merge_from(&src.use_autoclose);
 749    settings
 750        .use_auto_surround
 751        .merge_from(&src.use_auto_surround);
 752    settings
 753        .use_on_type_format
 754        .merge_from(&src.use_on_type_format);
 755    settings.auto_indent.merge_from(&src.auto_indent);
 756    settings
 757        .auto_indent_on_paste
 758        .merge_from(&src.auto_indent_on_paste);
 759    settings
 760        .always_treat_brackets_as_autoclosed
 761        .merge_from(&src.always_treat_brackets_as_autoclosed);
 762    settings.show_wrap_guides.merge_from(&src.show_wrap_guides);
 763    settings.wrap_guides.merge_from(&src.wrap_guides);
 764    settings.indent_guides.merge_from(&src.indent_guides);
 765    settings
 766        .code_actions_on_format
 767        .merge_from(&src.code_actions_on_format.clone());
 768    settings.linked_edits.merge_from(&src.linked_edits);
 769    settings.tasks.merge_from(&src.tasks.clone());
 770
 771    settings
 772        .preferred_line_length
 773        .merge_from(&src.preferred_line_length);
 774    settings.formatter.merge_from(&src.formatter.clone());
 775    settings.prettier.merge_from(&src.prettier.clone());
 776    settings
 777        .jsx_tag_auto_close
 778        .merge_from(&src.jsx_tag_auto_close.clone());
 779    settings
 780        .format_on_save
 781        .merge_from(&src.format_on_save.clone());
 782    settings
 783        .remove_trailing_whitespace_on_save
 784        .merge_from(&src.remove_trailing_whitespace_on_save);
 785    settings
 786        .ensure_final_newline_on_save
 787        .merge_from(&src.ensure_final_newline_on_save);
 788    settings
 789        .enable_language_server
 790        .merge_from(&src.enable_language_server);
 791    settings
 792        .language_servers
 793        .merge_from(&src.language_servers.clone());
 794    settings.allow_rewrap.merge_from(&src.allow_rewrap);
 795    settings
 796        .show_edit_predictions
 797        .merge_from(&src.show_edit_predictions);
 798    settings
 799        .edit_predictions_disabled_in
 800        .merge_from(&src.edit_predictions_disabled_in.clone());
 801    settings.show_whitespaces.merge_from(&src.show_whitespaces);
 802    settings
 803        .whitespace_map
 804        .merge_from(&src.whitespace_map.clone());
 805    settings
 806        .extend_comment_on_newline
 807        .merge_from(&src.extend_comment_on_newline);
 808    settings.inlay_hints.merge_from(&src.inlay_hints.clone());
 809    settings
 810        .show_completions_on_input
 811        .merge_from(&src.show_completions_on_input);
 812    settings
 813        .show_completion_documentation
 814        .merge_from(&src.show_completion_documentation);
 815    settings.completions.merge_from(&src.completions.clone());
 816}
 817
 818/// Allows to enable/disable formatting with Prettier
 819/// and configure default Prettier, used when no project-level Prettier installation is found.
 820/// Prettier formatting is disabled by default.
 821#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi)]
 822pub struct PrettierSettings {
 823    /// Enables or disables formatting with Prettier for a given language.
 824    #[serde(default)]
 825    pub allowed: bool,
 826
 827    /// Forces Prettier integration to use a specific parser name when formatting files with the language.
 828    #[serde(default)]
 829    pub parser: Option<String>,
 830
 831    /// Forces Prettier integration to use specific plugins when formatting files with the language.
 832    /// The default Prettier will be installed with these plugins.
 833    #[serde(default)]
 834    #[settings_ui(skip)]
 835    pub plugins: HashSet<String>,
 836
 837    /// Default Prettier options, in the format as in package.json section for Prettier.
 838    /// If project installs Prettier via its package.json, these options will be ignored.
 839    #[serde(flatten)]
 840    #[settings_ui(skip)]
 841    pub options: HashMap<String, serde_json::Value>,
 842}
 843
 844#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi)]
 845pub struct JsxTagAutoCloseSettings {
 846    /// Enables or disables auto-closing of JSX tags.
 847    #[serde(default)]
 848    pub enabled: bool,
 849}
 850
 851#[cfg(test)]
 852mod tests {
 853    use super::*;
 854    use gpui::TestAppContext;
 855
 856    #[gpui::test]
 857    fn test_edit_predictions_enabled_for_file(cx: &mut TestAppContext) {
 858        use crate::TestFile;
 859        use std::path::PathBuf;
 860
 861        let cx = cx.app.borrow_mut();
 862
 863        let build_settings = |globs: &[&str]| -> EditPredictionSettings {
 864            EditPredictionSettings {
 865                disabled_globs: globs
 866                    .iter()
 867                    .map(|glob_str| {
 868                        #[cfg(windows)]
 869                        let glob_str = {
 870                            let mut g = String::new();
 871
 872                            if glob_str.starts_with('/') {
 873                                g.push_str("C:");
 874                            }
 875
 876                            g.push_str(&glob_str.replace('/', "\\"));
 877                            g
 878                        };
 879                        #[cfg(windows)]
 880                        let glob_str = glob_str.as_str();
 881                        let expanded_glob_str = shellexpand::tilde(glob_str).into_owned();
 882                        DisabledGlob {
 883                            matcher: globset::Glob::new(&expanded_glob_str)
 884                                .unwrap()
 885                                .compile_matcher(),
 886                            is_absolute: Path::new(&expanded_glob_str).is_absolute(),
 887                        }
 888                    })
 889                    .collect(),
 890                ..Default::default()
 891            }
 892        };
 893
 894        const WORKTREE_NAME: &str = "project";
 895        let make_test_file = |segments: &[&str]| -> Arc<dyn File> {
 896            let mut path_buf = PathBuf::new();
 897            path_buf.extend(segments);
 898
 899            Arc::new(TestFile {
 900                path: path_buf.as_path().into(),
 901                root_name: WORKTREE_NAME.to_string(),
 902                local_root: Some(PathBuf::from(if cfg!(windows) {
 903                    "C:\\absolute\\"
 904                } else {
 905                    "/absolute/"
 906                })),
 907            })
 908        };
 909
 910        let test_file = make_test_file(&["src", "test", "file.rs"]);
 911
 912        // Test relative globs
 913        let settings = build_settings(&["*.rs"]);
 914        assert!(!settings.enabled_for_file(&test_file, &cx));
 915        let settings = build_settings(&["*.txt"]);
 916        assert!(settings.enabled_for_file(&test_file, &cx));
 917
 918        // Test absolute globs
 919        let settings = build_settings(&["/absolute/**/*.rs"]);
 920        assert!(!settings.enabled_for_file(&test_file, &cx));
 921        let settings = build_settings(&["/other/**/*.rs"]);
 922        assert!(settings.enabled_for_file(&test_file, &cx));
 923
 924        // Test exact path match relative
 925        let settings = build_settings(&["src/test/file.rs"]);
 926        assert!(!settings.enabled_for_file(&test_file, &cx));
 927        let settings = build_settings(&["src/test/otherfile.rs"]);
 928        assert!(settings.enabled_for_file(&test_file, &cx));
 929
 930        // Test exact path match absolute
 931        let settings = build_settings(&[&format!("/absolute/{}/src/test/file.rs", WORKTREE_NAME)]);
 932        assert!(!settings.enabled_for_file(&test_file, &cx));
 933        let settings = build_settings(&["/other/test/otherfile.rs"]);
 934        assert!(settings.enabled_for_file(&test_file, &cx));
 935
 936        // Test * glob
 937        let settings = build_settings(&["*"]);
 938        assert!(!settings.enabled_for_file(&test_file, &cx));
 939        let settings = build_settings(&["*.txt"]);
 940        assert!(settings.enabled_for_file(&test_file, &cx));
 941
 942        // Test **/* glob
 943        let settings = build_settings(&["**/*"]);
 944        assert!(!settings.enabled_for_file(&test_file, &cx));
 945        let settings = build_settings(&["other/**/*"]);
 946        assert!(settings.enabled_for_file(&test_file, &cx));
 947
 948        // Test directory/** glob
 949        let settings = build_settings(&["src/**"]);
 950        assert!(!settings.enabled_for_file(&test_file, &cx));
 951
 952        let test_file_root: Arc<dyn File> = Arc::new(TestFile {
 953            path: PathBuf::from("file.rs").as_path().into(),
 954            root_name: WORKTREE_NAME.to_string(),
 955            local_root: Some(PathBuf::from("/absolute/")),
 956        });
 957        assert!(settings.enabled_for_file(&test_file_root, &cx));
 958
 959        let settings = build_settings(&["other/**"]);
 960        assert!(settings.enabled_for_file(&test_file, &cx));
 961
 962        // Test **/directory/* glob
 963        let settings = build_settings(&["**/test/*"]);
 964        assert!(!settings.enabled_for_file(&test_file, &cx));
 965        let settings = build_settings(&["**/other/*"]);
 966        assert!(settings.enabled_for_file(&test_file, &cx));
 967
 968        // Test multiple globs
 969        let settings = build_settings(&["*.rs", "*.txt", "src/**"]);
 970        assert!(!settings.enabled_for_file(&test_file, &cx));
 971        let settings = build_settings(&["*.txt", "*.md", "other/**"]);
 972        assert!(settings.enabled_for_file(&test_file, &cx));
 973
 974        // Test dot files
 975        let dot_file = make_test_file(&[".config", "settings.json"]);
 976        let settings = build_settings(&[".*/**"]);
 977        assert!(!settings.enabled_for_file(&dot_file, &cx));
 978
 979        let dot_env_file = make_test_file(&[".env"]);
 980        let settings = build_settings(&[".env"]);
 981        assert!(!settings.enabled_for_file(&dot_env_file, &cx));
 982
 983        // Test tilde expansion
 984        let home = shellexpand::tilde("~").into_owned();
 985        let home_file = make_test_file(&[&home, "test.rs"]);
 986        let settings = build_settings(&["~/test.rs"]);
 987        assert!(!settings.enabled_for_file(&home_file, &cx));
 988    }
 989
 990    #[test]
 991    fn test_resolve_language_servers() {
 992        fn language_server_names(names: &[&str]) -> Vec<LanguageServerName> {
 993            names
 994                .iter()
 995                .copied()
 996                .map(|name| LanguageServerName(name.to_string().into()))
 997                .collect::<Vec<_>>()
 998        }
 999
1000        let available_language_servers = language_server_names(&[
1001            "typescript-language-server",
1002            "biome",
1003            "deno",
1004            "eslint",
1005            "tailwind",
1006        ]);
1007
1008        // A value of just `["..."]` is the same as taking all of the available language servers.
1009        assert_eq!(
1010            LanguageSettings::resolve_language_servers(
1011                &[LanguageSettings::REST_OF_LANGUAGE_SERVERS.into()],
1012                &available_language_servers,
1013            ),
1014            available_language_servers
1015        );
1016
1017        // Referencing one of the available language servers will change its order.
1018        assert_eq!(
1019            LanguageSettings::resolve_language_servers(
1020                &[
1021                    "biome".into(),
1022                    LanguageSettings::REST_OF_LANGUAGE_SERVERS.into(),
1023                    "deno".into()
1024                ],
1025                &available_language_servers
1026            ),
1027            language_server_names(&[
1028                "biome",
1029                "typescript-language-server",
1030                "eslint",
1031                "tailwind",
1032                "deno",
1033            ])
1034        );
1035
1036        // Negating an available language server removes it from the list.
1037        assert_eq!(
1038            LanguageSettings::resolve_language_servers(
1039                &[
1040                    "deno".into(),
1041                    "!typescript-language-server".into(),
1042                    "!biome".into(),
1043                    LanguageSettings::REST_OF_LANGUAGE_SERVERS.into()
1044                ],
1045                &available_language_servers
1046            ),
1047            language_server_names(&["deno", "eslint", "tailwind"])
1048        );
1049
1050        // Adding a language server not in the list of available language servers adds it to the list.
1051        assert_eq!(
1052            LanguageSettings::resolve_language_servers(
1053                &[
1054                    "my-cool-language-server".into(),
1055                    LanguageSettings::REST_OF_LANGUAGE_SERVERS.into()
1056                ],
1057                &available_language_servers
1058            ),
1059            language_server_names(&[
1060                "my-cool-language-server",
1061                "typescript-language-server",
1062                "biome",
1063                "deno",
1064                "eslint",
1065                "tailwind",
1066            ])
1067        );
1068    }
1069}