language_settings.rs

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