language_settings.rs

  1//! Provides `language`-related settings.
  2
  3use crate::{File, Language};
  4use anyhow::Result;
  5use collections::{HashMap, HashSet};
  6use globset::GlobMatcher;
  7use gpui::AppContext;
  8use schemars::{
  9    schema::{InstanceType, ObjectValidation, Schema, SchemaObject},
 10    JsonSchema,
 11};
 12use serde::{Deserialize, Serialize};
 13use settings::{Settings, SettingsLocation};
 14use std::{num::NonZeroU32, path::Path, sync::Arc};
 15
 16impl<'a> Into<SettingsLocation<'a>> for &'a dyn File {
 17    fn into(self) -> SettingsLocation<'a> {
 18        SettingsLocation {
 19            worktree_id: self.worktree_id(),
 20            path: self.path().as_ref(),
 21        }
 22    }
 23}
 24
 25/// Initializes the language settings.
 26pub fn init(cx: &mut AppContext) {
 27    AllLanguageSettings::register(cx);
 28}
 29
 30/// Returns the settings for the specified language from the provided file.
 31pub fn language_settings<'a>(
 32    language: Option<&Arc<Language>>,
 33    file: Option<&Arc<dyn File>>,
 34    cx: &'a AppContext,
 35) -> &'a LanguageSettings {
 36    let language_name = language.map(|l| l.name());
 37    all_language_settings(file, cx).language(language_name.as_deref())
 38}
 39
 40/// Returns the settings for all languages from the provided file.
 41pub fn all_language_settings<'a>(
 42    file: Option<&Arc<dyn File>>,
 43    cx: &'a AppContext,
 44) -> &'a AllLanguageSettings {
 45    let location = file.map(|f| f.as_ref().into());
 46    AllLanguageSettings::get(location, cx)
 47}
 48
 49/// The settings for all languages.
 50#[derive(Debug, Clone)]
 51pub struct AllLanguageSettings {
 52    /// The settings for GitHub Copilot.
 53    pub copilot: CopilotSettings,
 54    defaults: LanguageSettings,
 55    languages: HashMap<Arc<str>, LanguageSettings>,
 56    pub(crate) file_types: HashMap<Arc<str>, Vec<String>>,
 57}
 58
 59/// The settings for a particular language.
 60#[derive(Debug, Clone, Deserialize)]
 61pub struct LanguageSettings {
 62    /// How many columns a tab should occupy.
 63    pub tab_size: NonZeroU32,
 64    /// Whether to indent lines using tab characters, as opposed to multiple
 65    /// spaces.
 66    pub hard_tabs: bool,
 67    /// How to soft-wrap long lines of text.
 68    pub soft_wrap: SoftWrap,
 69    /// The column at which to soft-wrap lines, for buffers where soft-wrap
 70    /// is enabled.
 71    pub preferred_line_length: u32,
 72    /// Whether to show wrap guides in the editor. Setting this to true will
 73    /// show a guide at the 'preferred_line_length' value if softwrap is set to
 74    /// 'preferred_line_length', and will show any additional guides as specified
 75    /// by the 'wrap_guides' setting.
 76    pub show_wrap_guides: bool,
 77    /// Character counts at which to show wrap guides in the editor.
 78    pub wrap_guides: Vec<usize>,
 79    /// Whether or not to perform a buffer format before saving.
 80    pub format_on_save: FormatOnSave,
 81    /// Whether or not to remove any trailing whitespace from lines of a buffer
 82    /// before saving it.
 83    pub remove_trailing_whitespace_on_save: bool,
 84    /// Whether or not to ensure there's a single newline at the end of a buffer
 85    /// when saving it.
 86    pub ensure_final_newline_on_save: bool,
 87    /// How to perform a buffer format.
 88    pub formatter: Formatter,
 89    /// Zed's Prettier integration settings.
 90    /// If Prettier is enabled, Zed will use this its Prettier instance for any applicable file, if
 91    /// the project has no other Prettier installed.
 92    pub prettier: HashMap<String, serde_json::Value>,
 93    /// Whether to use language servers to provide code intelligence.
 94    pub enable_language_server: bool,
 95    /// Controls whether Copilot provides suggestion immediately (true)
 96    /// or waits for a `copilot::Toggle` (false).
 97    pub show_copilot_suggestions: bool,
 98    /// Whether to show tabs and spaces in the editor.
 99    pub show_whitespaces: ShowWhitespaceSetting,
100    /// Whether to start a new line with a comment when a previous line is a comment as well.
101    pub extend_comment_on_newline: bool,
102    /// Inlay hint related settings.
103    pub inlay_hints: InlayHintSettings,
104    /// Whether to automatically close brackets.
105    pub use_autoclose: bool,
106    /// Which code actions to run on save
107    pub code_actions_on_format: HashMap<String, bool>,
108}
109
110/// The settings for [GitHub Copilot](https://github.com/features/copilot).
111#[derive(Clone, Debug, Default)]
112pub struct CopilotSettings {
113    /// Whether Copilot is enabled.
114    pub feature_enabled: bool,
115    /// A list of globs representing files that Copilot should be disabled for.
116    pub disabled_globs: Vec<GlobMatcher>,
117}
118
119/// The settings for all languages.
120#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
121pub struct AllLanguageSettingsContent {
122    /// The settings for enabling/disabling features.
123    #[serde(default)]
124    pub features: Option<FeaturesContent>,
125    /// The settings for GitHub Copilot.
126    #[serde(default)]
127    pub copilot: Option<CopilotSettingsContent>,
128    /// The default language settings.
129    #[serde(flatten)]
130    pub defaults: LanguageSettingsContent,
131    /// The settings for individual languages.
132    #[serde(default, alias = "language_overrides")]
133    pub languages: HashMap<Arc<str>, LanguageSettingsContent>,
134    /// Settings for associating file extensions and filenames
135    /// with languages.
136    #[serde(default)]
137    pub file_types: HashMap<Arc<str>, Vec<String>>,
138}
139
140/// The settings for a particular language.
141#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
142pub struct LanguageSettingsContent {
143    /// How many columns a tab should occupy.
144    ///
145    /// Default: 4
146    #[serde(default)]
147    pub tab_size: Option<NonZeroU32>,
148    /// Whether to indent lines using tab characters, as opposed to multiple
149    /// spaces.
150    ///
151    /// Default: false
152    #[serde(default)]
153    pub hard_tabs: Option<bool>,
154    /// How to soft-wrap long lines of text.
155    ///
156    /// Default: none
157    #[serde(default)]
158    pub soft_wrap: Option<SoftWrap>,
159    /// The column at which to soft-wrap lines, for buffers where soft-wrap
160    /// is enabled.
161    ///
162    /// Default: 80
163    #[serde(default)]
164    pub preferred_line_length: Option<u32>,
165    /// Whether to show wrap guides in the editor. Setting this to true will
166    /// show a guide at the 'preferred_line_length' value if softwrap is set to
167    /// 'preferred_line_length', and will show any additional guides as specified
168    /// by the 'wrap_guides' setting.
169    ///
170    /// Default: true
171    #[serde(default)]
172    pub show_wrap_guides: Option<bool>,
173    /// Character counts at which to show wrap guides in the editor.
174    ///
175    /// Default: []
176    #[serde(default)]
177    pub wrap_guides: Option<Vec<usize>>,
178    /// Whether or not to perform a buffer format before saving.
179    ///
180    /// Default: on
181    #[serde(default)]
182    pub format_on_save: Option<FormatOnSave>,
183    /// Whether or not to remove any trailing whitespace from lines of a buffer
184    /// before saving it.
185    ///
186    /// Default: true
187    #[serde(default)]
188    pub remove_trailing_whitespace_on_save: Option<bool>,
189    /// Whether or not to ensure there's a single newline at the end of a buffer
190    /// when saving it.
191    ///
192    /// Default: true
193    #[serde(default)]
194    pub ensure_final_newline_on_save: Option<bool>,
195    /// How to perform a buffer format.
196    ///
197    /// Default: auto
198    #[serde(default)]
199    pub formatter: Option<Formatter>,
200    /// Zed's Prettier integration settings.
201    /// If Prettier is enabled, Zed will use this its Prettier instance for any applicable file, if
202    /// the project has no other Prettier installed.
203    ///
204    /// Default: {}
205    #[serde(default)]
206    pub prettier: Option<HashMap<String, serde_json::Value>>,
207    /// Whether to use language servers to provide code intelligence.
208    ///
209    /// Default: true
210    #[serde(default)]
211    pub enable_language_server: Option<bool>,
212    /// Controls whether Copilot provides suggestion immediately (true)
213    /// or waits for a `copilot::Toggle` (false).
214    ///
215    /// Default: true
216    #[serde(default)]
217    pub show_copilot_suggestions: Option<bool>,
218    /// Whether to show tabs and spaces in the editor.
219    #[serde(default)]
220    pub show_whitespaces: Option<ShowWhitespaceSetting>,
221    /// Whether to start a new line with a comment when a previous line is a comment as well.
222    ///
223    /// Default: true
224    #[serde(default)]
225    pub extend_comment_on_newline: Option<bool>,
226    /// Inlay hint related settings.
227    #[serde(default)]
228    pub inlay_hints: Option<InlayHintSettings>,
229    /// Whether to automatically type closing characters for you. For example,
230    /// when you type (, Zed will automatically add a closing ) at the correct position.
231    ///
232    /// Default: true
233    pub use_autoclose: Option<bool>,
234
235    /// Which code actions to run on save
236    ///
237    /// Default: {} (or {"source.organizeImports": true} for Go).
238    pub code_actions_on_format: Option<HashMap<String, bool>>,
239}
240
241/// The contents of the GitHub Copilot settings.
242#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
243pub struct CopilotSettingsContent {
244    /// A list of globs representing files that Copilot should be disabled for.
245    #[serde(default)]
246    pub disabled_globs: Option<Vec<String>>,
247}
248
249/// The settings for enabling/disabling features.
250#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
251#[serde(rename_all = "snake_case")]
252pub struct FeaturesContent {
253    /// Whether the GitHub Copilot feature is enabled.
254    pub copilot: Option<bool>,
255}
256
257/// Controls the soft-wrapping behavior in the editor.
258#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
259#[serde(rename_all = "snake_case")]
260pub enum SoftWrap {
261    /// Do not soft wrap.
262    None,
263    /// Soft wrap lines that overflow the editor
264    EditorWidth,
265    /// Soft wrap lines at the preferred line length
266    PreferredLineLength,
267}
268
269/// Controls the behavior of formatting files when they are saved.
270#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
271#[serde(rename_all = "snake_case")]
272pub enum FormatOnSave {
273    /// Files should be formatted on save.
274    On,
275    /// Files should not be formatted on save.
276    Off,
277    /// Files should be formatted using the current language server.
278    LanguageServer,
279    /// The external program to use to format the files on save.
280    External {
281        /// The external program to run.
282        command: Arc<str>,
283        /// The arguments to pass to the program.
284        arguments: Arc<[String]>,
285    },
286}
287
288/// Controls how whitespace should be displayedin the editor.
289#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
290#[serde(rename_all = "snake_case")]
291pub enum ShowWhitespaceSetting {
292    /// Draw whitespace only for the selected text.
293    Selection,
294    /// Do not draw any tabs or spaces.
295    None,
296    /// Draw all invisible symbols.
297    All,
298}
299
300/// Controls which formatter should be used when formatting code.
301#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
302#[serde(rename_all = "snake_case")]
303pub enum Formatter {
304    /// Format files using Zed's Prettier integration (if applicable),
305    /// or falling back to formatting via language server.
306    #[default]
307    Auto,
308    /// Format code using the current language server.
309    LanguageServer,
310    /// Format code using Zed's Prettier integration.
311    Prettier,
312    /// Format code using an external command.
313    External {
314        /// The external program to run.
315        command: Arc<str>,
316        /// The arguments to pass to the program.
317        arguments: Arc<[String]>,
318    },
319}
320
321/// The settings for inlay hints.
322#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
323pub struct InlayHintSettings {
324    /// Global switch to toggle hints on and off.
325    ///
326    /// Default: false
327    #[serde(default)]
328    pub enabled: bool,
329    /// Whether type hints should be shown.
330    ///
331    /// Default: true
332    #[serde(default = "default_true")]
333    pub show_type_hints: bool,
334    /// Whether parameter hints should be shown.
335    ///
336    /// Default: true
337    #[serde(default = "default_true")]
338    pub show_parameter_hints: bool,
339    /// Whether other hints should be shown.
340    ///
341    /// Default: true
342    #[serde(default = "default_true")]
343    pub show_other_hints: bool,
344    /// Whether or not to debounce inlay hints updates after buffer edits.
345    ///
346    /// Set to 0 to disable debouncing.
347    ///
348    /// Default: 700
349    #[serde(default = "edit_debounce_ms")]
350    pub edit_debounce_ms: u64,
351    /// Whether or not to debounce inlay hints updates after buffer scrolls.
352    ///
353    /// Set to 0 to disable debouncing.
354    ///
355    /// Default: 50
356    #[serde(default = "scroll_debounce_ms")]
357    pub scroll_debounce_ms: u64,
358}
359
360fn default_true() -> bool {
361    true
362}
363
364fn edit_debounce_ms() -> u64 {
365    700
366}
367
368fn scroll_debounce_ms() -> u64 {
369    50
370}
371
372impl InlayHintSettings {
373    /// Returns the kinds of inlay hints that are enabled based on the settings.
374    pub fn enabled_inlay_hint_kinds(&self) -> HashSet<Option<InlayHintKind>> {
375        let mut kinds = HashSet::default();
376        if self.show_type_hints {
377            kinds.insert(Some(InlayHintKind::Type));
378        }
379        if self.show_parameter_hints {
380            kinds.insert(Some(InlayHintKind::Parameter));
381        }
382        if self.show_other_hints {
383            kinds.insert(None);
384        }
385        kinds
386    }
387}
388
389impl AllLanguageSettings {
390    /// Returns the [`LanguageSettings`] for the language with the specified name.
391    pub fn language<'a>(&'a self, language_name: Option<&str>) -> &'a LanguageSettings {
392        if let Some(name) = language_name {
393            if let Some(overrides) = self.languages.get(name) {
394                return overrides;
395            }
396        }
397        &self.defaults
398    }
399
400    /// Returns whether GitHub Copilot is enabled for the given path.
401    pub fn copilot_enabled_for_path(&self, path: &Path) -> bool {
402        !self
403            .copilot
404            .disabled_globs
405            .iter()
406            .any(|glob| glob.is_match(path))
407    }
408
409    /// Returns whether GitHub Copilot is enabled for the given language and path.
410    pub fn copilot_enabled(&self, language: Option<&Arc<Language>>, path: Option<&Path>) -> bool {
411        if !self.copilot.feature_enabled {
412            return false;
413        }
414
415        if let Some(path) = path {
416            if !self.copilot_enabled_for_path(path) {
417                return false;
418            }
419        }
420
421        self.language(language.map(|l| l.name()).as_deref())
422            .show_copilot_suggestions
423    }
424}
425
426/// The kind of an inlay hint.
427#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
428pub enum InlayHintKind {
429    /// An inlay hint for a type.
430    Type,
431    /// An inlay hint for a parameter.
432    Parameter,
433}
434
435impl InlayHintKind {
436    /// Returns the [`InlayHintKind`] from the given name.
437    ///
438    /// Returns `None` if `name` does not match any of the expected
439    /// string representations.
440    pub fn from_name(name: &str) -> Option<Self> {
441        match name {
442            "type" => Some(InlayHintKind::Type),
443            "parameter" => Some(InlayHintKind::Parameter),
444            _ => None,
445        }
446    }
447
448    /// Returns the name of this [`InlayHintKind`].
449    pub fn name(&self) -> &'static str {
450        match self {
451            InlayHintKind::Type => "type",
452            InlayHintKind::Parameter => "parameter",
453        }
454    }
455}
456
457impl settings::Settings for AllLanguageSettings {
458    const KEY: Option<&'static str> = None;
459
460    type FileContent = AllLanguageSettingsContent;
461
462    fn load(
463        default_value: &Self::FileContent,
464        user_settings: &[&Self::FileContent],
465        _: &mut AppContext,
466    ) -> Result<Self> {
467        // A default is provided for all settings.
468        let mut defaults: LanguageSettings =
469            serde_json::from_value(serde_json::to_value(&default_value.defaults)?)?;
470
471        let mut languages = HashMap::default();
472        for (language_name, settings) in &default_value.languages {
473            let mut language_settings = defaults.clone();
474            merge_settings(&mut language_settings, settings);
475            languages.insert(language_name.clone(), language_settings);
476        }
477
478        let mut copilot_enabled = default_value
479            .features
480            .as_ref()
481            .and_then(|f| f.copilot)
482            .ok_or_else(Self::missing_default)?;
483        let mut copilot_globs = default_value
484            .copilot
485            .as_ref()
486            .and_then(|c| c.disabled_globs.as_ref())
487            .ok_or_else(Self::missing_default)?;
488
489        for user_settings in user_settings {
490            if let Some(copilot) = user_settings.features.as_ref().and_then(|f| f.copilot) {
491                copilot_enabled = copilot;
492            }
493            if let Some(globs) = user_settings
494                .copilot
495                .as_ref()
496                .and_then(|f| f.disabled_globs.as_ref())
497            {
498                copilot_globs = globs;
499            }
500
501            // A user's global settings override the default global settings and
502            // all default language-specific settings.
503            merge_settings(&mut defaults, &user_settings.defaults);
504            for language_settings in languages.values_mut() {
505                merge_settings(language_settings, &user_settings.defaults);
506            }
507
508            // A user's language-specific settings override default language-specific settings.
509            for (language_name, user_language_settings) in &user_settings.languages {
510                merge_settings(
511                    languages
512                        .entry(language_name.clone())
513                        .or_insert_with(|| defaults.clone()),
514                    user_language_settings,
515                );
516            }
517        }
518
519        let mut file_types: HashMap<Arc<str>, Vec<String>> = HashMap::default();
520        for user_file_types in user_settings.iter().map(|s| &s.file_types) {
521            for (language, suffixes) in user_file_types {
522                file_types
523                    .entry(language.clone())
524                    .or_default()
525                    .extend_from_slice(suffixes);
526            }
527        }
528
529        Ok(Self {
530            copilot: CopilotSettings {
531                feature_enabled: copilot_enabled,
532                disabled_globs: copilot_globs
533                    .iter()
534                    .filter_map(|g| Some(globset::Glob::new(g).ok()?.compile_matcher()))
535                    .collect(),
536            },
537            defaults,
538            languages,
539            file_types,
540        })
541    }
542
543    fn json_schema(
544        generator: &mut schemars::gen::SchemaGenerator,
545        params: &settings::SettingsJsonSchemaParams,
546        _: &AppContext,
547    ) -> schemars::schema::RootSchema {
548        let mut root_schema = generator.root_schema_for::<Self::FileContent>();
549
550        // Create a schema for a 'languages overrides' object, associating editor
551        // settings with specific languages.
552        assert!(root_schema
553            .definitions
554            .contains_key("LanguageSettingsContent"));
555
556        let languages_object_schema = SchemaObject {
557            instance_type: Some(InstanceType::Object.into()),
558            object: Some(Box::new(ObjectValidation {
559                properties: params
560                    .language_names
561                    .iter()
562                    .map(|name| {
563                        (
564                            name.clone(),
565                            Schema::new_ref("#/definitions/LanguageSettingsContent".into()),
566                        )
567                    })
568                    .collect(),
569                ..Default::default()
570            })),
571            ..Default::default()
572        };
573
574        root_schema
575            .definitions
576            .extend([("Languages".into(), languages_object_schema.into())]);
577
578        root_schema
579            .schema
580            .object
581            .as_mut()
582            .unwrap()
583            .properties
584            .extend([
585                (
586                    "languages".to_owned(),
587                    Schema::new_ref("#/definitions/Languages".into()),
588                ),
589                // For backward compatibility
590                (
591                    "language_overrides".to_owned(),
592                    Schema::new_ref("#/definitions/Languages".into()),
593                ),
594            ]);
595
596        root_schema
597    }
598}
599
600fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent) {
601    merge(&mut settings.tab_size, src.tab_size);
602    merge(&mut settings.hard_tabs, src.hard_tabs);
603    merge(&mut settings.soft_wrap, src.soft_wrap);
604    merge(&mut settings.use_autoclose, src.use_autoclose);
605    merge(&mut settings.show_wrap_guides, src.show_wrap_guides);
606    merge(&mut settings.wrap_guides, src.wrap_guides.clone());
607    merge(
608        &mut settings.code_actions_on_format,
609        src.code_actions_on_format.clone(),
610    );
611
612    merge(
613        &mut settings.preferred_line_length,
614        src.preferred_line_length,
615    );
616    merge(&mut settings.formatter, src.formatter.clone());
617    merge(&mut settings.prettier, src.prettier.clone());
618    merge(&mut settings.format_on_save, src.format_on_save.clone());
619    merge(
620        &mut settings.remove_trailing_whitespace_on_save,
621        src.remove_trailing_whitespace_on_save,
622    );
623    merge(
624        &mut settings.ensure_final_newline_on_save,
625        src.ensure_final_newline_on_save,
626    );
627    merge(
628        &mut settings.enable_language_server,
629        src.enable_language_server,
630    );
631    merge(
632        &mut settings.show_copilot_suggestions,
633        src.show_copilot_suggestions,
634    );
635    merge(&mut settings.show_whitespaces, src.show_whitespaces);
636    merge(
637        &mut settings.extend_comment_on_newline,
638        src.extend_comment_on_newline,
639    );
640    merge(&mut settings.inlay_hints, src.inlay_hints);
641    fn merge<T>(target: &mut T, value: Option<T>) {
642        if let Some(value) = value {
643            *target = value;
644        }
645    }
646}