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 // 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, 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, 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 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
245 ///
246 /// Default: {} (or {"source.organizeImports": true} for Go).
247 pub code_actions_on_format: Option<HashMap<String, bool>>,
248}
249
250/// The contents of the GitHub Copilot settings.
251#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
252pub struct CopilotSettingsContent {
253 /// A list of globs representing files that Copilot should be disabled for.
254 #[serde(default)]
255 pub disabled_globs: Option<Vec<String>>,
256}
257
258/// The settings for enabling/disabling features.
259#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
260#[serde(rename_all = "snake_case")]
261pub struct FeaturesContent {
262 /// Whether the GitHub Copilot feature is enabled.
263 pub copilot: Option<bool>,
264}
265
266/// Controls the soft-wrapping behavior in the editor.
267#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
268#[serde(rename_all = "snake_case")]
269pub enum SoftWrap {
270 /// Do not soft wrap.
271 None,
272 /// Soft wrap lines that overflow the editor
273 EditorWidth,
274 /// Soft wrap lines at the preferred line length
275 PreferredLineLength,
276}
277
278/// Controls the behavior of formatting files when they are saved.
279#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
280#[serde(rename_all = "snake_case")]
281pub enum FormatOnSave {
282 /// Files should be formatted on save.
283 On,
284 /// Files should not be formatted on save.
285 Off,
286 /// Files should be formatted using the current language server.
287 LanguageServer,
288 /// The external program to use to format the files on save.
289 External {
290 /// The external program to run.
291 command: Arc<str>,
292 /// The arguments to pass to the program.
293 arguments: Arc<[String]>,
294 },
295}
296
297/// Controls how whitespace should be displayedin the editor.
298#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
299#[serde(rename_all = "snake_case")]
300pub enum ShowWhitespaceSetting {
301 /// Draw whitespace only for the selected text.
302 Selection,
303 /// Do not draw any tabs or spaces.
304 None,
305 /// Draw all invisible symbols.
306 All,
307}
308
309/// Controls which formatter should be used when formatting code.
310#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
311#[serde(rename_all = "snake_case")]
312pub enum Formatter {
313 /// Format files using Zed's Prettier integration (if applicable),
314 /// or falling back to formatting via language server.
315 #[default]
316 Auto,
317 /// Format code using the current language server.
318 LanguageServer,
319 /// Format code using Zed's Prettier integration.
320 Prettier,
321 /// Format code using an external command.
322 External {
323 /// The external program to run.
324 command: Arc<str>,
325 /// The arguments to pass to the program.
326 arguments: Arc<[String]>,
327 },
328}
329
330/// The settings for inlay hints.
331#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
332pub struct InlayHintSettings {
333 /// Global switch to toggle hints on and off.
334 ///
335 /// Default: false
336 #[serde(default)]
337 pub enabled: bool,
338 /// Whether type hints should be shown.
339 ///
340 /// Default: true
341 #[serde(default = "default_true")]
342 pub show_type_hints: bool,
343 /// Whether parameter hints should be shown.
344 ///
345 /// Default: true
346 #[serde(default = "default_true")]
347 pub show_parameter_hints: bool,
348 /// Whether other hints should be shown.
349 ///
350 /// Default: true
351 #[serde(default = "default_true")]
352 pub show_other_hints: bool,
353 /// Whether or not to debounce inlay hints updates after buffer edits.
354 ///
355 /// Set to 0 to disable debouncing.
356 ///
357 /// Default: 700
358 #[serde(default = "edit_debounce_ms")]
359 pub edit_debounce_ms: u64,
360 /// Whether or not to debounce inlay hints updates after buffer scrolls.
361 ///
362 /// Set to 0 to disable debouncing.
363 ///
364 /// Default: 50
365 #[serde(default = "scroll_debounce_ms")]
366 pub scroll_debounce_ms: u64,
367}
368
369fn default_true() -> bool {
370 true
371}
372
373fn edit_debounce_ms() -> u64 {
374 700
375}
376
377fn scroll_debounce_ms() -> u64 {
378 50
379}
380
381impl InlayHintSettings {
382 /// Returns the kinds of inlay hints that are enabled based on the settings.
383 pub fn enabled_inlay_hint_kinds(&self) -> HashSet<Option<InlayHintKind>> {
384 let mut kinds = HashSet::default();
385 if self.show_type_hints {
386 kinds.insert(Some(InlayHintKind::Type));
387 }
388 if self.show_parameter_hints {
389 kinds.insert(Some(InlayHintKind::Parameter));
390 }
391 if self.show_other_hints {
392 kinds.insert(None);
393 }
394 kinds
395 }
396}
397
398impl AllLanguageSettings {
399 /// Returns the [`LanguageSettings`] for the language with the specified name.
400 pub fn language<'a>(&'a self, language_name: Option<&str>) -> &'a LanguageSettings {
401 if let Some(name) = language_name {
402 if let Some(overrides) = self.languages.get(name) {
403 return overrides;
404 }
405 }
406 &self.defaults
407 }
408
409 /// Returns whether GitHub Copilot is enabled for the given path.
410 pub fn copilot_enabled_for_path(&self, path: &Path) -> bool {
411 !self
412 .copilot
413 .disabled_globs
414 .iter()
415 .any(|glob| glob.is_match(path))
416 }
417
418 /// Returns whether GitHub Copilot is enabled for the given language and path.
419 pub fn copilot_enabled(&self, language: Option<&Arc<Language>>, path: Option<&Path>) -> bool {
420 if !self.copilot.feature_enabled {
421 return false;
422 }
423
424 if let Some(path) = path {
425 if !self.copilot_enabled_for_path(path) {
426 return false;
427 }
428 }
429
430 self.language(language.map(|l| l.name()).as_deref())
431 .show_copilot_suggestions
432 }
433}
434
435/// The kind of an inlay hint.
436#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
437pub enum InlayHintKind {
438 /// An inlay hint for a type.
439 Type,
440 /// An inlay hint for a parameter.
441 Parameter,
442}
443
444impl InlayHintKind {
445 /// Returns the [`InlayHintKind`] from the given name.
446 ///
447 /// Returns `None` if `name` does not match any of the expected
448 /// string representations.
449 pub fn from_name(name: &str) -> Option<Self> {
450 match name {
451 "type" => Some(InlayHintKind::Type),
452 "parameter" => Some(InlayHintKind::Parameter),
453 _ => None,
454 }
455 }
456
457 /// Returns the name of this [`InlayHintKind`].
458 pub fn name(&self) -> &'static str {
459 match self {
460 InlayHintKind::Type => "type",
461 InlayHintKind::Parameter => "parameter",
462 }
463 }
464}
465
466impl settings::Settings for AllLanguageSettings {
467 const KEY: Option<&'static str> = None;
468
469 type FileContent = AllLanguageSettingsContent;
470
471 fn load(
472 default_value: &Self::FileContent,
473 user_settings: &[&Self::FileContent],
474 _: &mut AppContext,
475 ) -> Result<Self> {
476 // A default is provided for all settings.
477 let mut defaults: LanguageSettings =
478 serde_json::from_value(serde_json::to_value(&default_value.defaults)?)?;
479
480 let mut languages = HashMap::default();
481 for (language_name, settings) in &default_value.languages {
482 let mut language_settings = defaults.clone();
483 merge_settings(&mut language_settings, settings);
484 languages.insert(language_name.clone(), language_settings);
485 }
486
487 let mut copilot_enabled = default_value
488 .features
489 .as_ref()
490 .and_then(|f| f.copilot)
491 .ok_or_else(Self::missing_default)?;
492 let mut copilot_globs = default_value
493 .copilot
494 .as_ref()
495 .and_then(|c| c.disabled_globs.as_ref())
496 .ok_or_else(Self::missing_default)?;
497
498 for user_settings in user_settings {
499 if let Some(copilot) = user_settings.features.as_ref().and_then(|f| f.copilot) {
500 copilot_enabled = copilot;
501 }
502 if let Some(globs) = user_settings
503 .copilot
504 .as_ref()
505 .and_then(|f| f.disabled_globs.as_ref())
506 {
507 copilot_globs = globs;
508 }
509
510 // A user's global settings override the default global settings and
511 // all default language-specific settings.
512 merge_settings(&mut defaults, &user_settings.defaults);
513 for language_settings in languages.values_mut() {
514 merge_settings(language_settings, &user_settings.defaults);
515 }
516
517 // A user's language-specific settings override default language-specific settings.
518 for (language_name, user_language_settings) in &user_settings.languages {
519 merge_settings(
520 languages
521 .entry(language_name.clone())
522 .or_insert_with(|| defaults.clone()),
523 user_language_settings,
524 );
525 }
526 }
527
528 let mut file_types: HashMap<Arc<str>, Vec<String>> = HashMap::default();
529 for user_file_types in user_settings.iter().map(|s| &s.file_types) {
530 for (language, suffixes) in user_file_types {
531 file_types
532 .entry(language.clone())
533 .or_default()
534 .extend_from_slice(suffixes);
535 }
536 }
537
538 Ok(Self {
539 copilot: CopilotSettings {
540 feature_enabled: copilot_enabled,
541 disabled_globs: copilot_globs
542 .iter()
543 .filter_map(|g| Some(globset::Glob::new(g).ok()?.compile_matcher()))
544 .collect(),
545 },
546 defaults,
547 languages,
548 file_types,
549 })
550 }
551
552 fn json_schema(
553 generator: &mut schemars::gen::SchemaGenerator,
554 params: &settings::SettingsJsonSchemaParams,
555 _: &AppContext,
556 ) -> schemars::schema::RootSchema {
557 let mut root_schema = generator.root_schema_for::<Self::FileContent>();
558
559 // Create a schema for a 'languages overrides' object, associating editor
560 // settings with specific languages.
561 assert!(root_schema
562 .definitions
563 .contains_key("LanguageSettingsContent"));
564
565 let languages_object_schema = SchemaObject {
566 instance_type: Some(InstanceType::Object.into()),
567 object: Some(Box::new(ObjectValidation {
568 properties: params
569 .language_names
570 .iter()
571 .map(|name| {
572 (
573 name.clone(),
574 Schema::new_ref("#/definitions/LanguageSettingsContent".into()),
575 )
576 })
577 .collect(),
578 ..Default::default()
579 })),
580 ..Default::default()
581 };
582
583 root_schema
584 .definitions
585 .extend([("Languages".into(), languages_object_schema.into())]);
586
587 root_schema
588 .schema
589 .object
590 .as_mut()
591 .unwrap()
592 .properties
593 .extend([
594 (
595 "languages".to_owned(),
596 Schema::new_ref("#/definitions/Languages".into()),
597 ),
598 // For backward compatibility
599 (
600 "language_overrides".to_owned(),
601 Schema::new_ref("#/definitions/Languages".into()),
602 ),
603 ]);
604
605 root_schema
606 }
607}
608
609fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent) {
610 merge(&mut settings.tab_size, src.tab_size);
611 merge(&mut settings.hard_tabs, src.hard_tabs);
612 merge(&mut settings.soft_wrap, src.soft_wrap);
613 merge(&mut settings.use_autoclose, src.use_autoclose);
614 merge(
615 &mut settings.always_treat_brackets_as_autoclosed,
616 src.always_treat_brackets_as_autoclosed,
617 );
618 merge(&mut settings.show_wrap_guides, src.show_wrap_guides);
619 merge(&mut settings.wrap_guides, src.wrap_guides.clone());
620 merge(
621 &mut settings.code_actions_on_format,
622 src.code_actions_on_format.clone(),
623 );
624
625 merge(
626 &mut settings.preferred_line_length,
627 src.preferred_line_length,
628 );
629 merge(&mut settings.formatter, src.formatter.clone());
630 merge(&mut settings.prettier, src.prettier.clone());
631 merge(&mut settings.format_on_save, src.format_on_save.clone());
632 merge(
633 &mut settings.remove_trailing_whitespace_on_save,
634 src.remove_trailing_whitespace_on_save,
635 );
636 merge(
637 &mut settings.ensure_final_newline_on_save,
638 src.ensure_final_newline_on_save,
639 );
640 merge(
641 &mut settings.enable_language_server,
642 src.enable_language_server,
643 );
644 merge(
645 &mut settings.show_copilot_suggestions,
646 src.show_copilot_suggestions,
647 );
648 merge(&mut settings.show_whitespaces, src.show_whitespaces);
649 merge(
650 &mut settings.extend_comment_on_newline,
651 src.extend_comment_on_newline,
652 );
653 merge(&mut settings.inlay_hints, src.inlay_hints);
654 fn merge<T>(target: &mut T, value: Option<T>) {
655 if let Some(value) = value {
656 *target = value;
657 }
658 }
659}