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