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}