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        let edit_predictions_mode = all_languages.edit_predictions.as_ref().unwrap().mode;
 434
 435        let disabled_globs: HashSet<&String> = all_languages
 436            .edit_predictions
 437            .as_ref()
 438            .unwrap()
 439            .disabled_globs
 440            .as_ref()
 441            .unwrap()
 442            .iter()
 443            .collect();
 444
 445        let copilot_settings = all_languages
 446            .edit_predictions
 447            .as_ref()
 448            .map(|settings| CopilotSettings {
 449                proxy: settings.copilot.proxy.clone(),
 450                proxy_no_verify: settings.copilot.proxy_no_verify,
 451                enterprise_uri: settings.copilot.enterprise_uri.clone(),
 452            })
 453            .unwrap_or_default();
 454
 455        let enabled_in_text_threads = all_languages
 456            .edit_predictions
 457            .as_ref()
 458            .map(|settings| settings.enabled_in_text_threads)
 459            .unwrap_or(true);
 460
 461        let mut file_types: FxHashMap<Arc<str>, GlobSet> = FxHashMap::default();
 462        let mut file_globs: FxHashMap<Arc<str>, Vec<String>> = FxHashMap::default();
 463
 464        for (language, patterns) in &all_languages.file_types {
 465            let mut builder = GlobSetBuilder::new();
 466
 467            for pattern in patterns {
 468                builder.add(Glob::new(pattern).unwrap());
 469            }
 470
 471            file_types.insert(language.clone(), builder.build().unwrap());
 472            file_globs.insert(language.clone(), patterns.clone());
 473        }
 474
 475        Self {
 476            edit_predictions: EditPredictionSettings {
 477                provider: if let Some(provider) = edit_prediction_provider {
 478                    provider
 479                } else {
 480                    EditPredictionProvider::None
 481                },
 482                disabled_globs: disabled_globs
 483                    .iter()
 484                    .filter_map(|g| {
 485                        let expanded_g = shellexpand::tilde(g).into_owned();
 486                        Some(DisabledGlob {
 487                            matcher: globset::Glob::new(&expanded_g).ok()?.compile_matcher(),
 488                            is_absolute: Path::new(&expanded_g).is_absolute(),
 489                        })
 490                    })
 491                    .collect(),
 492                mode: edit_predictions_mode,
 493                copilot: copilot_settings,
 494                enabled_in_text_threads,
 495            },
 496            defaults: default_language_settings,
 497            languages,
 498            file_types,
 499            file_globs,
 500        }
 501    }
 502
 503    fn refine(&mut self, content: &SettingsContent, _cx: &mut App) {
 504        let all_languages = &content.project.all_languages;
 505        if let Some(provider) = all_languages
 506            .features
 507            .as_ref()
 508            .and_then(|f| f.edit_prediction_provider)
 509        {
 510            self.edit_predictions.provider = provider;
 511        }
 512
 513        if let Some(edit_predictions) = all_languages.edit_predictions.as_ref() {
 514            self.edit_predictions.mode = edit_predictions.mode;
 515            self.edit_predictions.enabled_in_text_threads =
 516                edit_predictions.enabled_in_text_threads;
 517
 518            if let Some(disabled_globs) = edit_predictions.disabled_globs.as_ref() {
 519                self.edit_predictions
 520                    .disabled_globs
 521                    .extend(disabled_globs.iter().filter_map(|g| {
 522                        let expanded_g = shellexpand::tilde(g).into_owned();
 523                        Some(DisabledGlob {
 524                            matcher: globset::Glob::new(&expanded_g).ok()?.compile_matcher(),
 525                            is_absolute: Path::new(&expanded_g).is_absolute(),
 526                        })
 527                    }));
 528            }
 529        }
 530
 531        if let Some(proxy) = all_languages
 532            .edit_predictions
 533            .as_ref()
 534            .and_then(|settings| settings.copilot.proxy.clone())
 535        {
 536            self.edit_predictions.copilot.proxy = Some(proxy);
 537        }
 538
 539        if let Some(proxy_no_verify) = all_languages
 540            .edit_predictions
 541            .as_ref()
 542            .and_then(|settings| settings.copilot.proxy_no_verify)
 543        {
 544            self.edit_predictions.copilot.proxy_no_verify = Some(proxy_no_verify);
 545        }
 546
 547        if let Some(enterprise_uri) = all_languages
 548            .edit_predictions
 549            .as_ref()
 550            .and_then(|settings| settings.copilot.enterprise_uri.clone())
 551        {
 552            self.edit_predictions.copilot.enterprise_uri = Some(enterprise_uri);
 553        }
 554
 555        // A user's global settings override the default global settings and
 556        // all default language-specific settings.
 557        merge_settings(&mut self.defaults, &all_languages.defaults);
 558        for language_settings in self.languages.values_mut() {
 559            merge_settings(language_settings, &all_languages.defaults);
 560        }
 561
 562        // A user's language-specific settings override default language-specific settings.
 563        for (language_name, user_language_settings) in &all_languages.languages.0 {
 564            merge_settings(
 565                self.languages
 566                    .entry(LanguageName(language_name.clone()))
 567                    .or_insert_with(|| self.defaults.clone()),
 568                user_language_settings,
 569            );
 570        }
 571
 572        for (language, patterns) in &all_languages.file_types {
 573            let mut builder = GlobSetBuilder::new();
 574
 575            let default_value = self.file_globs.get(&language.clone());
 576
 577            // Merge the default value with the user's value.
 578            if let Some(patterns) = default_value {
 579                for pattern in patterns {
 580                    if let Some(glob) = Glob::new(pattern).log_err() {
 581                        builder.add(glob);
 582                    }
 583                }
 584            }
 585
 586            for pattern in patterns {
 587                if let Some(glob) = Glob::new(pattern).log_err() {
 588                    builder.add(glob);
 589                }
 590            }
 591
 592            self.file_globs
 593                .entry(language.clone())
 594                .or_default()
 595                .extend(patterns.clone());
 596
 597            if let Some(matcher) = builder.build().log_err() {
 598                self.file_types.insert(language.clone(), matcher);
 599            }
 600        }
 601    }
 602
 603    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
 604        let d = &mut current.project.all_languages.defaults;
 605        if let Some(size) = vscode
 606            .read_value("editor.tabSize")
 607            .and_then(|v| v.as_u64())
 608            .and_then(|n| NonZeroU32::new(n as u32))
 609        {
 610            d.tab_size = Some(size);
 611        }
 612        if let Some(v) = vscode.read_bool("editor.insertSpaces") {
 613            d.hard_tabs = Some(!v);
 614        }
 615
 616        vscode.enum_setting("editor.wordWrap", &mut d.soft_wrap, |s| match s {
 617            "on" => Some(SoftWrap::EditorWidth),
 618            "wordWrapColumn" => Some(SoftWrap::PreferLine),
 619            "bounded" => Some(SoftWrap::Bounded),
 620            "off" => Some(SoftWrap::None),
 621            _ => None,
 622        });
 623        vscode.u32_setting("editor.wordWrapColumn", &mut d.preferred_line_length);
 624
 625        if let Some(arr) = vscode
 626            .read_value("editor.rulers")
 627            .and_then(|v| v.as_array())
 628            .map(|v| v.iter().map(|n| n.as_u64().map(|n| n as usize)).collect())
 629        {
 630            d.wrap_guides = arr;
 631        }
 632        if let Some(b) = vscode.read_bool("editor.guides.indentation") {
 633            if let Some(guide_settings) = d.indent_guides.as_mut() {
 634                guide_settings.enabled = b;
 635            } else {
 636                d.indent_guides = Some(IndentGuideSettings {
 637                    enabled: b,
 638                    ..Default::default()
 639                });
 640            }
 641        }
 642
 643        if let Some(b) = vscode.read_bool("editor.guides.formatOnSave") {
 644            d.format_on_save = Some(if b {
 645                FormatOnSave::On
 646            } else {
 647                FormatOnSave::Off
 648            });
 649        }
 650        vscode.bool_setting(
 651            "editor.trimAutoWhitespace",
 652            &mut d.remove_trailing_whitespace_on_save,
 653        );
 654        vscode.bool_setting(
 655            "files.insertFinalNewline",
 656            &mut d.ensure_final_newline_on_save,
 657        );
 658        vscode.bool_setting("editor.inlineSuggest.enabled", &mut d.show_edit_predictions);
 659        vscode.enum_setting("editor.renderWhitespace", &mut d.show_whitespaces, |s| {
 660            Some(match s {
 661                "boundary" => ShowWhitespaceSetting::Boundary,
 662                "trailing" => ShowWhitespaceSetting::Trailing,
 663                "selection" => ShowWhitespaceSetting::Selection,
 664                "all" => ShowWhitespaceSetting::All,
 665                _ => ShowWhitespaceSetting::None,
 666            })
 667        });
 668        vscode.enum_setting(
 669            "editor.autoSurround",
 670            &mut d.use_auto_surround,
 671            |s| match s {
 672                "languageDefined" | "quotes" | "brackets" => Some(true),
 673                "never" => Some(false),
 674                _ => None,
 675            },
 676        );
 677        vscode.bool_setting("editor.formatOnType", &mut d.use_on_type_format);
 678        vscode.bool_setting("editor.linkedEditing", &mut d.linked_edits);
 679        vscode.bool_setting("editor.formatOnPaste", &mut d.auto_indent_on_paste);
 680        vscode.bool_setting(
 681            "editor.suggestOnTriggerCharacters",
 682            &mut d.show_completions_on_input,
 683        );
 684        if let Some(b) = vscode.read_bool("editor.suggest.showWords") {
 685            let mode = if b {
 686                WordsCompletionMode::Enabled
 687            } else {
 688                WordsCompletionMode::Disabled
 689            };
 690            if let Some(completion_settings) = d.completions.as_mut() {
 691                completion_settings.words = mode;
 692            } else {
 693                d.completions = Some(CompletionSettings {
 694                    words: mode,
 695                    words_min_length: 3,
 696                    lsp: true,
 697                    lsp_fetch_timeout_ms: 0,
 698                    lsp_insert_mode: LspInsertMode::ReplaceSuffix,
 699                });
 700            }
 701        }
 702        // TODO: pull ^ out into helper and reuse for per-language settings
 703
 704        // vscodes file association map is inverted from ours, so we flip the mapping before merging
 705        let mut associations: HashMap<Arc<str>, Vec<String>> = HashMap::default();
 706        if let Some(map) = vscode
 707            .read_value("files.associations")
 708            .and_then(|v| v.as_object())
 709        {
 710            for (k, v) in map {
 711                let Some(v) = v.as_str() else { continue };
 712                associations.entry(v.into()).or_default().push(k.clone());
 713            }
 714        }
 715
 716        // TODO: do we want to merge imported globs per filetype? for now we'll just replace
 717        current
 718            .project
 719            .all_languages
 720            .file_types
 721            .extend(associations);
 722
 723        // cursor global ignore list applies to cursor-tab, so transfer it to edit_predictions.disabled_globs
 724        if let Some(disabled_globs) = vscode
 725            .read_value("cursor.general.globalCursorIgnoreList")
 726            .and_then(|v| v.as_array())
 727        {
 728            current
 729                .project
 730                .all_languages
 731                .edit_predictions
 732                .get_or_insert_default()
 733                .disabled_globs
 734                .get_or_insert_default()
 735                .extend(
 736                    disabled_globs
 737                        .iter()
 738                        .filter_map(|glob| glob.as_str())
 739                        .map(|s| s.to_string()),
 740                );
 741        }
 742    }
 743}
 744
 745fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent) {
 746    settings.tab_size.merge_from(&src.tab_size);
 747    settings.tab_size = settings
 748        .tab_size
 749        .clamp(NonZeroU32::new(1).unwrap(), NonZeroU32::new(16).unwrap());
 750
 751    settings.hard_tabs.merge_from(&src.hard_tabs);
 752    settings.soft_wrap.merge_from(&src.soft_wrap);
 753    settings.use_autoclose.merge_from(&src.use_autoclose);
 754    settings
 755        .use_auto_surround
 756        .merge_from(&src.use_auto_surround);
 757    settings
 758        .use_on_type_format
 759        .merge_from(&src.use_on_type_format);
 760    settings.auto_indent.merge_from(&src.auto_indent);
 761    settings
 762        .auto_indent_on_paste
 763        .merge_from(&src.auto_indent_on_paste);
 764    settings
 765        .always_treat_brackets_as_autoclosed
 766        .merge_from(&src.always_treat_brackets_as_autoclosed);
 767    settings.show_wrap_guides.merge_from(&src.show_wrap_guides);
 768    settings.wrap_guides.merge_from(&src.wrap_guides);
 769    settings.indent_guides.merge_from(&src.indent_guides);
 770    settings
 771        .code_actions_on_format
 772        .merge_from(&src.code_actions_on_format.clone());
 773    settings.linked_edits.merge_from(&src.linked_edits);
 774    settings.tasks.merge_from(&src.tasks.clone());
 775
 776    settings
 777        .preferred_line_length
 778        .merge_from(&src.preferred_line_length);
 779    settings.formatter.merge_from(&src.formatter.clone());
 780    settings.prettier.merge_from(&src.prettier.clone());
 781    settings
 782        .jsx_tag_auto_close
 783        .merge_from(&src.jsx_tag_auto_close.clone());
 784    settings
 785        .format_on_save
 786        .merge_from(&src.format_on_save.clone());
 787    settings
 788        .remove_trailing_whitespace_on_save
 789        .merge_from(&src.remove_trailing_whitespace_on_save);
 790    settings
 791        .ensure_final_newline_on_save
 792        .merge_from(&src.ensure_final_newline_on_save);
 793    settings
 794        .enable_language_server
 795        .merge_from(&src.enable_language_server);
 796    settings
 797        .language_servers
 798        .merge_from(&src.language_servers.clone());
 799    settings.allow_rewrap.merge_from(&src.allow_rewrap);
 800    settings
 801        .show_edit_predictions
 802        .merge_from(&src.show_edit_predictions);
 803    settings
 804        .edit_predictions_disabled_in
 805        .merge_from(&src.edit_predictions_disabled_in.clone());
 806    settings.show_whitespaces.merge_from(&src.show_whitespaces);
 807    settings
 808        .whitespace_map
 809        .merge_from(&src.whitespace_map.clone());
 810    settings
 811        .extend_comment_on_newline
 812        .merge_from(&src.extend_comment_on_newline);
 813    settings.inlay_hints.merge_from(&src.inlay_hints.clone());
 814    settings
 815        .show_completions_on_input
 816        .merge_from(&src.show_completions_on_input);
 817    settings
 818        .show_completion_documentation
 819        .merge_from(&src.show_completion_documentation);
 820    settings.completions.merge_from(&src.completions.clone());
 821}
 822
 823/// Allows to enable/disable formatting with Prettier
 824/// and configure default Prettier, used when no project-level Prettier installation is found.
 825/// Prettier formatting is disabled by default.
 826#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi)]
 827pub struct PrettierSettings {
 828    /// Enables or disables formatting with Prettier for a given language.
 829    #[serde(default)]
 830    pub allowed: bool,
 831
 832    /// Forces Prettier integration to use a specific parser name when formatting files with the language.
 833    #[serde(default)]
 834    pub parser: Option<String>,
 835
 836    /// Forces Prettier integration to use specific plugins when formatting files with the language.
 837    /// The default Prettier will be installed with these plugins.
 838    #[serde(default)]
 839    #[settings_ui(skip)]
 840    pub plugins: HashSet<String>,
 841
 842    /// Default Prettier options, in the format as in package.json section for Prettier.
 843    /// If project installs Prettier via its package.json, these options will be ignored.
 844    #[serde(flatten)]
 845    #[settings_ui(skip)]
 846    pub options: HashMap<String, serde_json::Value>,
 847}
 848
 849#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi)]
 850pub struct JsxTagAutoCloseSettings {
 851    /// Enables or disables auto-closing of JSX tags.
 852    #[serde(default)]
 853    pub enabled: bool,
 854}
 855
 856#[cfg(test)]
 857mod tests {
 858    use super::*;
 859    use gpui::TestAppContext;
 860
 861    #[gpui::test]
 862    fn test_edit_predictions_enabled_for_file(cx: &mut TestAppContext) {
 863        use crate::TestFile;
 864        use std::path::PathBuf;
 865
 866        let cx = cx.app.borrow_mut();
 867
 868        let build_settings = |globs: &[&str]| -> EditPredictionSettings {
 869            EditPredictionSettings {
 870                disabled_globs: globs
 871                    .iter()
 872                    .map(|glob_str| {
 873                        #[cfg(windows)]
 874                        let glob_str = {
 875                            let mut g = String::new();
 876
 877                            if glob_str.starts_with('/') {
 878                                g.push_str("C:");
 879                            }
 880
 881                            g.push_str(&glob_str.replace('/', "\\"));
 882                            g
 883                        };
 884                        #[cfg(windows)]
 885                        let glob_str = glob_str.as_str();
 886                        let expanded_glob_str = shellexpand::tilde(glob_str).into_owned();
 887                        DisabledGlob {
 888                            matcher: globset::Glob::new(&expanded_glob_str)
 889                                .unwrap()
 890                                .compile_matcher(),
 891                            is_absolute: Path::new(&expanded_glob_str).is_absolute(),
 892                        }
 893                    })
 894                    .collect(),
 895                ..Default::default()
 896            }
 897        };
 898
 899        const WORKTREE_NAME: &str = "project";
 900        let make_test_file = |segments: &[&str]| -> Arc<dyn File> {
 901            let mut path_buf = PathBuf::new();
 902            path_buf.extend(segments);
 903
 904            Arc::new(TestFile {
 905                path: path_buf.as_path().into(),
 906                root_name: WORKTREE_NAME.to_string(),
 907                local_root: Some(PathBuf::from(if cfg!(windows) {
 908                    "C:\\absolute\\"
 909                } else {
 910                    "/absolute/"
 911                })),
 912            })
 913        };
 914
 915        let test_file = make_test_file(&["src", "test", "file.rs"]);
 916
 917        // Test relative globs
 918        let settings = build_settings(&["*.rs"]);
 919        assert!(!settings.enabled_for_file(&test_file, &cx));
 920        let settings = build_settings(&["*.txt"]);
 921        assert!(settings.enabled_for_file(&test_file, &cx));
 922
 923        // Test absolute globs
 924        let settings = build_settings(&["/absolute/**/*.rs"]);
 925        assert!(!settings.enabled_for_file(&test_file, &cx));
 926        let settings = build_settings(&["/other/**/*.rs"]);
 927        assert!(settings.enabled_for_file(&test_file, &cx));
 928
 929        // Test exact path match relative
 930        let settings = build_settings(&["src/test/file.rs"]);
 931        assert!(!settings.enabled_for_file(&test_file, &cx));
 932        let settings = build_settings(&["src/test/otherfile.rs"]);
 933        assert!(settings.enabled_for_file(&test_file, &cx));
 934
 935        // Test exact path match absolute
 936        let settings = build_settings(&[&format!("/absolute/{}/src/test/file.rs", WORKTREE_NAME)]);
 937        assert!(!settings.enabled_for_file(&test_file, &cx));
 938        let settings = build_settings(&["/other/test/otherfile.rs"]);
 939        assert!(settings.enabled_for_file(&test_file, &cx));
 940
 941        // Test * glob
 942        let settings = build_settings(&["*"]);
 943        assert!(!settings.enabled_for_file(&test_file, &cx));
 944        let settings = build_settings(&["*.txt"]);
 945        assert!(settings.enabled_for_file(&test_file, &cx));
 946
 947        // Test **/* glob
 948        let settings = build_settings(&["**/*"]);
 949        assert!(!settings.enabled_for_file(&test_file, &cx));
 950        let settings = build_settings(&["other/**/*"]);
 951        assert!(settings.enabled_for_file(&test_file, &cx));
 952
 953        // Test directory/** glob
 954        let settings = build_settings(&["src/**"]);
 955        assert!(!settings.enabled_for_file(&test_file, &cx));
 956
 957        let test_file_root: Arc<dyn File> = Arc::new(TestFile {
 958            path: PathBuf::from("file.rs").as_path().into(),
 959            root_name: WORKTREE_NAME.to_string(),
 960            local_root: Some(PathBuf::from("/absolute/")),
 961        });
 962        assert!(settings.enabled_for_file(&test_file_root, &cx));
 963
 964        let settings = build_settings(&["other/**"]);
 965        assert!(settings.enabled_for_file(&test_file, &cx));
 966
 967        // Test **/directory/* glob
 968        let settings = build_settings(&["**/test/*"]);
 969        assert!(!settings.enabled_for_file(&test_file, &cx));
 970        let settings = build_settings(&["**/other/*"]);
 971        assert!(settings.enabled_for_file(&test_file, &cx));
 972
 973        // Test multiple globs
 974        let settings = build_settings(&["*.rs", "*.txt", "src/**"]);
 975        assert!(!settings.enabled_for_file(&test_file, &cx));
 976        let settings = build_settings(&["*.txt", "*.md", "other/**"]);
 977        assert!(settings.enabled_for_file(&test_file, &cx));
 978
 979        // Test dot files
 980        let dot_file = make_test_file(&[".config", "settings.json"]);
 981        let settings = build_settings(&[".*/**"]);
 982        assert!(!settings.enabled_for_file(&dot_file, &cx));
 983
 984        let dot_env_file = make_test_file(&[".env"]);
 985        let settings = build_settings(&[".env"]);
 986        assert!(!settings.enabled_for_file(&dot_env_file, &cx));
 987
 988        // Test tilde expansion
 989        let home = shellexpand::tilde("~").into_owned();
 990        let home_file = make_test_file(&[&home, "test.rs"]);
 991        let settings = build_settings(&["~/test.rs"]);
 992        assert!(!settings.enabled_for_file(&home_file, &cx));
 993    }
 994
 995    #[test]
 996    fn test_resolve_language_servers() {
 997        fn language_server_names(names: &[&str]) -> Vec<LanguageServerName> {
 998            names
 999                .iter()
1000                .copied()
1001                .map(|name| LanguageServerName(name.to_string().into()))
1002                .collect::<Vec<_>>()
1003        }
1004
1005        let available_language_servers = language_server_names(&[
1006            "typescript-language-server",
1007            "biome",
1008            "deno",
1009            "eslint",
1010            "tailwind",
1011        ]);
1012
1013        // A value of just `["..."]` is the same as taking all of the available language servers.
1014        assert_eq!(
1015            LanguageSettings::resolve_language_servers(
1016                &[LanguageSettings::REST_OF_LANGUAGE_SERVERS.into()],
1017                &available_language_servers,
1018            ),
1019            available_language_servers
1020        );
1021
1022        // Referencing one of the available language servers will change its order.
1023        assert_eq!(
1024            LanguageSettings::resolve_language_servers(
1025                &[
1026                    "biome".into(),
1027                    LanguageSettings::REST_OF_LANGUAGE_SERVERS.into(),
1028                    "deno".into()
1029                ],
1030                &available_language_servers
1031            ),
1032            language_server_names(&[
1033                "biome",
1034                "typescript-language-server",
1035                "eslint",
1036                "tailwind",
1037                "deno",
1038            ])
1039        );
1040
1041        // Negating an available language server removes it from the list.
1042        assert_eq!(
1043            LanguageSettings::resolve_language_servers(
1044                &[
1045                    "deno".into(),
1046                    "!typescript-language-server".into(),
1047                    "!biome".into(),
1048                    LanguageSettings::REST_OF_LANGUAGE_SERVERS.into()
1049                ],
1050                &available_language_servers
1051            ),
1052            language_server_names(&["deno", "eslint", "tailwind"])
1053        );
1054
1055        // Adding a language server not in the list of available language servers adds it to the list.
1056        assert_eq!(
1057            LanguageSettings::resolve_language_servers(
1058                &[
1059                    "my-cool-language-server".into(),
1060                    LanguageSettings::REST_OF_LANGUAGE_SERVERS.into()
1061                ],
1062                &available_language_servers
1063            ),
1064            language_server_names(&[
1065                "my-cool-language-server",
1066                "typescript-language-server",
1067                "biome",
1068                "deno",
1069                "eslint",
1070                "tailwind",
1071            ])
1072        );
1073    }
1074}