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