1//! Provides `language`-related settings.
2
3use crate::{File, Language, LanguageName, LanguageServerName};
4use collections::{FxHashMap, HashMap, HashSet};
5use ec4rs::{
6 Properties as EditorconfigProperties,
7 property::{FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs},
8};
9use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
10use gpui::{App, Modifiers};
11use itertools::{Either, Itertools};
12use schemars::{JsonSchema, json_schema};
13use serde::{Deserialize, Serialize};
14
15pub use settings::{
16 CompletionSettings, EditPredictionProvider, EditPredictionsMode, FormatOnSave, Formatter,
17 FormatterList, IndentGuideSettings, InlayHintKind, LanguageSettingsContent, LspInsertMode,
18 RewrapBehavior, SelectedFormatter, ShowWhitespaceSetting, SoftWrap, WordsCompletionMode,
19};
20use settings::{
21 ParameterizedJsonSchema, Settings, SettingsContent, SettingsLocation, SettingsStore, SettingsUi,
22};
23use shellexpand;
24use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc};
25use util::MergeFrom;
26use util::{ResultExt, schemars::replace_subschema};
27
28/// Initializes the language settings.
29pub fn init(cx: &mut App) {
30 AllLanguageSettings::register(cx);
31}
32
33/// Returns the settings for the specified language from the provided file.
34pub fn language_settings<'a>(
35 language: Option<LanguageName>,
36 file: Option<&'a Arc<dyn File>>,
37 cx: &'a App,
38) -> Cow<'a, LanguageSettings> {
39 let location = file.map(|f| SettingsLocation {
40 worktree_id: f.worktree_id(cx),
41 path: f.path().as_ref(),
42 });
43 AllLanguageSettings::get(location, cx).language(location, language.as_ref(), cx)
44}
45
46/// Returns the settings for all languages from the provided file.
47pub fn all_language_settings<'a>(
48 file: Option<&'a Arc<dyn File>>,
49 cx: &'a App,
50) -> &'a AllLanguageSettings {
51 let location = file.map(|f| SettingsLocation {
52 worktree_id: f.worktree_id(cx),
53 path: f.path().as_ref(),
54 });
55 AllLanguageSettings::get(location, cx)
56}
57
58/// The settings for all languages.
59#[derive(Debug, Clone)]
60pub struct AllLanguageSettings {
61 /// The edit prediction settings.
62 pub edit_predictions: EditPredictionSettings,
63 pub defaults: LanguageSettings,
64 languages: HashMap<LanguageName, LanguageSettings>,
65 pub(crate) file_types: FxHashMap<Arc<str>, GlobSet>,
66 pub(crate) file_globs: FxHashMap<Arc<str>, Vec<String>>,
67}
68
69/// The settings for a particular language.
70#[derive(Debug, Clone)]
71pub struct LanguageSettings {
72 /// How many columns a tab should occupy.
73 pub tab_size: NonZeroU32,
74 /// Whether to indent lines using tab characters, as opposed to multiple
75 /// spaces.
76 pub hard_tabs: bool,
77 /// How to soft-wrap long lines of text.
78 pub soft_wrap: settings::SoftWrap,
79 /// The column at which to soft-wrap lines, for buffers where soft-wrap
80 /// is enabled.
81 pub preferred_line_length: u32,
82 /// Whether to show wrap guides (vertical rulers) in the editor.
83 /// Setting this to true will show a guide at the 'preferred_line_length' value
84 /// if softwrap is set to 'preferred_line_length', and will show any
85 /// additional guides as specified by the 'wrap_guides' setting.
86 pub show_wrap_guides: bool,
87 /// Character counts at which to show wrap guides (vertical rulers) in the editor.
88 pub wrap_guides: Vec<usize>,
89 /// Indent guide related settings.
90 /// todo!() shouldthis be not the content type?
91 pub indent_guides: IndentGuideSettings,
92 /// Whether or not to perform a buffer format before saving.
93 pub format_on_save: FormatOnSave,
94 /// Whether or not to remove any trailing whitespace from lines of a buffer
95 /// before saving it.
96 pub remove_trailing_whitespace_on_save: bool,
97 /// Whether or not to ensure there's a single newline at the end of a buffer
98 /// when saving it.
99 pub ensure_final_newline_on_save: bool,
100 /// How to perform a buffer format.
101 pub formatter: settings::SelectedFormatter,
102 /// Zed's Prettier integration settings.
103 pub prettier: settings::PrettierSettingsContent,
104 /// Whether to automatically close JSX tags.
105 pub jsx_tag_auto_close: settings::JsxTagAutoCloseSettings,
106 /// Whether to use language servers to provide code intelligence.
107 pub enable_language_server: bool,
108 /// The list of language servers to use (or disable) for this language.
109 ///
110 /// This array should consist of language server IDs, as well as the following
111 /// special tokens:
112 /// - `"!<language_server_id>"` - A language server ID prefixed with a `!` will be disabled.
113 /// - `"..."` - A placeholder to refer to the **rest** of the registered language servers for this language.
114 pub language_servers: Vec<String>,
115 /// Controls where the `editor::Rewrap` action is allowed for this language.
116 ///
117 /// Note: This setting has no effect in Vim mode, as rewrap is already
118 /// allowed everywhere.
119 pub allow_rewrap: RewrapBehavior,
120 /// Controls whether edit predictions are shown immediately (true)
121 /// or manually by triggering `editor::ShowEditPrediction` (false).
122 pub show_edit_predictions: bool,
123 /// Controls whether edit predictions are shown in the given language
124 /// scopes.
125 pub edit_predictions_disabled_in: Vec<String>,
126 /// Whether to show tabs and spaces in the editor.
127 pub show_whitespaces: settings::ShowWhitespaceSetting,
128 /// Visible characters used to render whitespace when show_whitespaces is enabled.
129 pub whitespace_map: settings::WhitespaceMap,
130 /// Whether to start a new line with a comment when a previous line is a comment as well.
131 pub extend_comment_on_newline: bool,
132 /// Inlay hint related settings.
133 pub inlay_hints: InlayHintSettings,
134 /// Whether to automatically close brackets.
135 pub use_autoclose: bool,
136 /// Whether to automatically surround text with brackets.
137 pub use_auto_surround: bool,
138 /// Whether to use additional LSP queries to format (and amend) the code after
139 /// every "trigger" symbol input, defined by LSP server capabilities.
140 pub use_on_type_format: bool,
141 /// Whether indentation should be adjusted based on the context whilst typing.
142 pub auto_indent: bool,
143 /// Whether indentation of pasted content should be adjusted based on the context.
144 pub auto_indent_on_paste: bool,
145 /// Controls how the editor handles the autoclosed characters.
146 pub always_treat_brackets_as_autoclosed: bool,
147 /// Which code actions to run on save
148 pub code_actions_on_format: HashMap<String, bool>,
149 /// Whether to perform linked edits
150 pub linked_edits: bool,
151 /// Task configuration for this language.
152 pub tasks: settings::LanguageTaskConfig,
153 /// Whether to pop the completions menu while typing in an editor without
154 /// explicitly requesting it.
155 pub show_completions_on_input: bool,
156 /// Whether to display inline and alongside documentation for items in the
157 /// completions menu.
158 pub show_completion_documentation: bool,
159 /// Completion settings for this language.
160 pub completions: settings::CompletionSettings,
161 /// Preferred debuggers for this language.
162 pub debuggers: Vec<String>,
163}
164
165impl LanguageSettings {
166 /// A token representing the rest of the available language servers.
167 const REST_OF_LANGUAGE_SERVERS: &'static str = "...";
168
169 /// Returns the customized list of language servers from the list of
170 /// available language servers.
171 pub fn customized_language_servers(
172 &self,
173 available_language_servers: &[LanguageServerName],
174 ) -> Vec<LanguageServerName> {
175 Self::resolve_language_servers(&self.language_servers, available_language_servers)
176 }
177
178 pub(crate) fn resolve_language_servers(
179 configured_language_servers: &[String],
180 available_language_servers: &[LanguageServerName],
181 ) -> Vec<LanguageServerName> {
182 let (disabled_language_servers, enabled_language_servers): (
183 Vec<LanguageServerName>,
184 Vec<LanguageServerName>,
185 ) = configured_language_servers.iter().partition_map(
186 |language_server| match language_server.strip_prefix('!') {
187 Some(disabled) => Either::Left(LanguageServerName(disabled.to_string().into())),
188 None => Either::Right(LanguageServerName(language_server.clone().into())),
189 },
190 );
191
192 let rest = available_language_servers
193 .iter()
194 .filter(|&available_language_server| {
195 !disabled_language_servers.contains(available_language_server)
196 && !enabled_language_servers.contains(available_language_server)
197 })
198 .cloned()
199 .collect::<Vec<_>>();
200
201 enabled_language_servers
202 .into_iter()
203 .flat_map(|language_server| {
204 if language_server.0.as_ref() == Self::REST_OF_LANGUAGE_SERVERS {
205 rest.clone()
206 } else {
207 vec![language_server]
208 }
209 })
210 .collect::<Vec<_>>()
211 }
212}
213
214// The settings for inlay hints.
215#[derive(Copy, Clone, Debug, PartialEq, Eq)]
216pub struct InlayHintSettings {
217 /// Global switch to toggle hints on and off.
218 ///
219 /// Default: false
220 pub enabled: bool,
221 /// Global switch to toggle inline values on and off when debugging.
222 ///
223 /// Default: true
224 pub show_value_hints: bool,
225 /// Whether type hints should be shown.
226 ///
227 /// Default: true
228 pub show_type_hints: bool,
229 /// Whether parameter hints should be shown.
230 ///
231 /// Default: true
232 pub show_parameter_hints: bool,
233 /// Whether other hints should be shown.
234 ///
235 /// Default: true
236 pub show_other_hints: bool,
237 /// Whether to show a background for inlay hints.
238 ///
239 /// If set to `true`, the background will use the `hint.background` color
240 /// from the current theme.
241 ///
242 /// Default: false
243 pub show_background: bool,
244 /// Whether or not to debounce inlay hints updates after buffer edits.
245 ///
246 /// Set to 0 to disable debouncing.
247 ///
248 /// Default: 700
249 pub edit_debounce_ms: u64,
250 /// Whether or not to debounce inlay hints updates after buffer scrolls.
251 ///
252 /// Set to 0 to disable debouncing.
253 ///
254 /// Default: 50
255 pub scroll_debounce_ms: u64,
256 /// Toggles inlay hints (hides or shows) when the user presses the modifiers specified.
257 /// If only a subset of the modifiers specified is pressed, hints are not toggled.
258 /// If no modifiers are specified, this is equivalent to `None`.
259 ///
260 /// Default: None
261 pub toggle_on_modifiers_press: Option<Modifiers>,
262}
263
264impl InlayHintSettings {
265 /// Returns the kinds of inlay hints that are enabled based on the settings.
266 pub fn enabled_inlay_hint_kinds(&self) -> HashSet<Option<InlayHintKind>> {
267 let mut kinds = HashSet::default();
268 if self.show_type_hints {
269 kinds.insert(Some(InlayHintKind::Type));
270 }
271 if self.show_parameter_hints {
272 kinds.insert(Some(InlayHintKind::Parameter));
273 }
274 if self.show_other_hints {
275 kinds.insert(None);
276 }
277 kinds
278 }
279}
280
281/// The settings for edit predictions, such as [GitHub Copilot](https://github.com/features/copilot)
282/// or [Supermaven](https://supermaven.com).
283#[derive(Clone, Debug, Default, SettingsUi)]
284pub struct EditPredictionSettings {
285 /// The provider that supplies edit predictions.
286 pub provider: settings::EditPredictionProvider,
287 /// A list of globs representing files that edit predictions should be disabled for.
288 /// This list adds to a pre-existing, sensible default set of globs.
289 /// Any additional ones you add are combined with them.
290 #[settings_ui(skip)]
291 pub disabled_globs: Vec<DisabledGlob>,
292 /// Configures how edit predictions are displayed in the buffer.
293 pub mode: settings::EditPredictionsMode,
294 /// Settings specific to GitHub Copilot.
295 pub copilot: CopilotSettings,
296 /// Whether edit predictions are enabled in the assistant panel.
297 /// This setting has no effect if globally disabled.
298 pub enabled_in_text_threads: bool,
299}
300
301impl EditPredictionSettings {
302 /// Returns whether edit predictions are enabled for the given path.
303 pub fn enabled_for_file(&self, file: &Arc<dyn File>, cx: &App) -> bool {
304 !self.disabled_globs.iter().any(|glob| {
305 if glob.is_absolute {
306 file.as_local()
307 .is_some_and(|local| glob.matcher.is_match(local.abs_path(cx)))
308 } else {
309 glob.matcher.is_match(file.path())
310 }
311 })
312 }
313}
314
315#[derive(Clone, Debug)]
316pub struct DisabledGlob {
317 matcher: GlobMatcher,
318 is_absolute: bool,
319}
320
321#[derive(Clone, Debug, Default, SettingsUi)]
322pub struct CopilotSettings {
323 /// HTTP/HTTPS proxy to use for Copilot.
324 #[settings_ui(skip)]
325 pub proxy: Option<String>,
326 /// Disable certificate verification for proxy (not recommended).
327 pub proxy_no_verify: Option<bool>,
328 /// Enterprise URI for Copilot.
329 #[settings_ui(skip)]
330 pub enterprise_uri: Option<String>,
331}
332
333inventory::submit! {
334 ParameterizedJsonSchema {
335 add_and_get_ref: |generator, params, _cx| {
336 let language_settings_content_ref = generator
337 .subschema_for::<LanguageSettingsContent>()
338 .to_value();
339 replace_subschema::<settings::LanguageToSettingsMap>(generator, || json_schema!({
340 "type": "object",
341 "properties": params
342 .language_names
343 .iter()
344 .map(|name| {
345 (
346 name.clone(),
347 language_settings_content_ref.clone(),
348 )
349 })
350 .collect::<serde_json::Map<_, _>>()
351 }))
352 }
353 }
354}
355
356impl AllLanguageSettings {
357 /// Returns the [`LanguageSettings`] for the language with the specified name.
358 pub fn language<'a>(
359 &'a self,
360 location: Option<SettingsLocation<'a>>,
361 language_name: Option<&LanguageName>,
362 cx: &'a App,
363 ) -> Cow<'a, LanguageSettings> {
364 let settings = language_name
365 .and_then(|name| self.languages.get(name))
366 .unwrap_or(&self.defaults);
367
368 let editorconfig_properties = location.and_then(|location| {
369 cx.global::<SettingsStore>()
370 .editorconfig_properties(location.worktree_id, location.path)
371 });
372 if let Some(editorconfig_properties) = editorconfig_properties {
373 let mut settings = settings.clone();
374 merge_with_editorconfig(&mut settings, &editorconfig_properties);
375 Cow::Owned(settings)
376 } else {
377 Cow::Borrowed(settings)
378 }
379 }
380
381 /// Returns whether edit predictions are enabled for the given path.
382 pub fn edit_predictions_enabled_for_file(&self, file: &Arc<dyn File>, cx: &App) -> bool {
383 self.edit_predictions.enabled_for_file(file, cx)
384 }
385
386 /// Returns whether edit predictions are enabled for the given language and path.
387 pub fn show_edit_predictions(&self, language: Option<&Arc<Language>>, cx: &App) -> bool {
388 self.language(None, language.map(|l| l.name()).as_ref(), cx)
389 .show_edit_predictions
390 }
391
392 /// Returns the edit predictions preview mode for the given language and path.
393 pub fn edit_predictions_mode(&self) -> EditPredictionsMode {
394 self.edit_predictions.mode
395 }
396}
397
398fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) {
399 let preferred_line_length = cfg.get::<MaxLineLen>().ok().and_then(|v| match v {
400 MaxLineLen::Value(u) => Some(u as u32),
401 MaxLineLen::Off => None,
402 });
403 let tab_size = cfg.get::<IndentSize>().ok().and_then(|v| match v {
404 IndentSize::Value(u) => NonZeroU32::new(u as u32),
405 IndentSize::UseTabWidth => cfg.get::<TabWidth>().ok().and_then(|w| match w {
406 TabWidth::Value(u) => NonZeroU32::new(u as u32),
407 }),
408 });
409 let hard_tabs = cfg
410 .get::<IndentStyle>()
411 .map(|v| v.eq(&IndentStyle::Tabs))
412 .ok();
413 let ensure_final_newline_on_save = cfg
414 .get::<FinalNewline>()
415 .map(|v| match v {
416 FinalNewline::Value(b) => b,
417 })
418 .ok();
419 let remove_trailing_whitespace_on_save = cfg
420 .get::<TrimTrailingWs>()
421 .map(|v| match v {
422 TrimTrailingWs::Value(b) => b,
423 })
424 .ok();
425 fn merge<T>(target: &mut T, value: Option<T>) {
426 if let Some(value) = value {
427 *target = value;
428 }
429 }
430 merge(&mut settings.preferred_line_length, preferred_line_length);
431 merge(&mut settings.tab_size, tab_size);
432 merge(&mut settings.hard_tabs, hard_tabs);
433 merge(
434 &mut settings.remove_trailing_whitespace_on_save,
435 remove_trailing_whitespace_on_save,
436 );
437 merge(
438 &mut settings.ensure_final_newline_on_save,
439 ensure_final_newline_on_save,
440 );
441}
442
443impl settings::Settings for AllLanguageSettings {
444 fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
445 let all_languages = &content.project.all_languages;
446 let defaults = all_languages.defaults.clone();
447 let inlay_hints = defaults.inlay_hints.unwrap();
448 let default_language_settings = LanguageSettings {
449 tab_size: defaults.tab_size.unwrap(),
450 hard_tabs: defaults.hard_tabs.unwrap(),
451 soft_wrap: defaults.soft_wrap.unwrap(),
452 preferred_line_length: defaults.preferred_line_length.unwrap(),
453 show_wrap_guides: defaults.show_wrap_guides.unwrap(),
454 wrap_guides: defaults.wrap_guides.unwrap(),
455 indent_guides: defaults.indent_guides.unwrap(),
456 format_on_save: defaults.format_on_save.unwrap(),
457 remove_trailing_whitespace_on_save: defaults
458 .remove_trailing_whitespace_on_save
459 .unwrap(),
460 ensure_final_newline_on_save: defaults.ensure_final_newline_on_save.unwrap(),
461 formatter: defaults.formatter.unwrap(),
462 prettier: defaults.prettier.unwrap(),
463 jsx_tag_auto_close: defaults.jsx_tag_auto_close.unwrap(),
464 enable_language_server: defaults.enable_language_server.unwrap(),
465 language_servers: defaults.language_servers.unwrap(),
466 allow_rewrap: defaults.allow_rewrap.unwrap(),
467 show_edit_predictions: defaults.show_edit_predictions.unwrap(),
468 edit_predictions_disabled_in: defaults.edit_predictions_disabled_in.unwrap(),
469 show_whitespaces: defaults.show_whitespaces.unwrap(),
470 whitespace_map: defaults.whitespace_map.unwrap(),
471 extend_comment_on_newline: defaults.extend_comment_on_newline.unwrap(),
472 inlay_hints: InlayHintSettings {
473 enabled: inlay_hints.enabled.unwrap(),
474 show_value_hints: inlay_hints.show_value_hints.unwrap(),
475 show_type_hints: inlay_hints.show_type_hints.unwrap(),
476 show_parameter_hints: inlay_hints.show_parameter_hints.unwrap(),
477 show_other_hints: inlay_hints.show_other_hints.unwrap(),
478 show_background: inlay_hints.show_background.unwrap(),
479 edit_debounce_ms: inlay_hints.edit_debounce_ms.unwrap(),
480 scroll_debounce_ms: inlay_hints.scroll_debounce_ms.unwrap(),
481 toggle_on_modifiers_press: inlay_hints.toggle_on_modifiers_press,
482 },
483 use_autoclose: defaults.use_autoclose.unwrap(),
484 use_auto_surround: defaults.use_auto_surround.unwrap(),
485 use_on_type_format: defaults.use_on_type_format.unwrap(),
486 auto_indent: defaults.auto_indent.unwrap(),
487 auto_indent_on_paste: defaults.auto_indent_on_paste.unwrap(),
488 always_treat_brackets_as_autoclosed: defaults
489 .always_treat_brackets_as_autoclosed
490 .unwrap(),
491 code_actions_on_format: defaults.code_actions_on_format.unwrap(),
492 linked_edits: defaults.linked_edits.unwrap(),
493 tasks: defaults.tasks.unwrap(),
494 show_completions_on_input: defaults.show_completions_on_input.unwrap(),
495 show_completion_documentation: defaults.show_completion_documentation.unwrap(),
496 completions: defaults.completions.unwrap(),
497 debuggers: defaults.debuggers.unwrap(),
498 };
499
500 let mut languages = HashMap::default();
501 for (language_name, settings) in &all_languages.languages.0 {
502 let mut language_settings = default_language_settings.clone();
503 merge_settings(&mut language_settings, settings);
504 languages.insert(LanguageName(language_name.clone()), language_settings);
505 }
506
507 let edit_prediction_provider = all_languages
508 .features
509 .as_ref()
510 .and_then(|f| f.edit_prediction_provider);
511
512 let edit_predictions = all_languages.edit_predictions.clone().unwrap();
513 let edit_predictions_mode = edit_predictions.mode.unwrap();
514
515 let disabled_globs: HashSet<&String> = edit_predictions
516 .disabled_globs
517 .as_ref()
518 .unwrap()
519 .iter()
520 .collect();
521
522 let copilot = edit_predictions.copilot.unwrap();
523 let copilot_settings = CopilotSettings {
524 proxy: copilot.proxy,
525 proxy_no_verify: copilot.proxy_no_verify,
526 enterprise_uri: copilot.enterprise_uri,
527 };
528
529 let enabled_in_text_threads = edit_predictions.enabled_in_text_threads.unwrap();
530
531 let mut file_types: FxHashMap<Arc<str>, GlobSet> = FxHashMap::default();
532 let mut file_globs: FxHashMap<Arc<str>, Vec<String>> = FxHashMap::default();
533
534 for (language, patterns) in &all_languages.file_types {
535 let mut builder = GlobSetBuilder::new();
536
537 for pattern in patterns {
538 builder.add(Glob::new(pattern).unwrap());
539 }
540
541 file_types.insert(language.clone(), builder.build().unwrap());
542 file_globs.insert(language.clone(), patterns.clone());
543 }
544
545 Self {
546 edit_predictions: EditPredictionSettings {
547 provider: if let Some(provider) = edit_prediction_provider {
548 provider
549 } else {
550 EditPredictionProvider::None
551 },
552 disabled_globs: disabled_globs
553 .iter()
554 .filter_map(|g| {
555 let expanded_g = shellexpand::tilde(g).into_owned();
556 Some(DisabledGlob {
557 matcher: globset::Glob::new(&expanded_g).ok()?.compile_matcher(),
558 is_absolute: Path::new(&expanded_g).is_absolute(),
559 })
560 })
561 .collect(),
562 mode: edit_predictions_mode,
563 copilot: copilot_settings,
564 enabled_in_text_threads,
565 },
566 defaults: default_language_settings,
567 languages,
568 file_types,
569 file_globs,
570 }
571 }
572
573 fn refine(&mut self, content: &SettingsContent, _cx: &mut App) {
574 let all_languages = &content.project.all_languages;
575 if let Some(provider) = all_languages
576 .features
577 .as_ref()
578 .and_then(|f| f.edit_prediction_provider)
579 {
580 self.edit_predictions.provider = provider;
581 }
582
583 if let Some(edit_predictions) = all_languages.edit_predictions.as_ref() {
584 self.edit_predictions
585 .mode
586 .merge_from(&edit_predictions.mode);
587 self.edit_predictions
588 .enabled_in_text_threads
589 .merge_from(&edit_predictions.enabled_in_text_threads);
590
591 if let Some(disabled_globs) = edit_predictions.disabled_globs.as_ref() {
592 self.edit_predictions
593 .disabled_globs
594 .extend(disabled_globs.iter().filter_map(|g| {
595 let expanded_g = shellexpand::tilde(g).into_owned();
596 Some(DisabledGlob {
597 matcher: globset::Glob::new(&expanded_g).ok()?.compile_matcher(),
598 is_absolute: Path::new(&expanded_g).is_absolute(),
599 })
600 }));
601 }
602 }
603
604 if let Some(proxy) = all_languages
605 .edit_predictions
606 .as_ref()
607 .and_then(|settings| settings.copilot.as_ref()?.proxy.clone())
608 {
609 self.edit_predictions.copilot.proxy = Some(proxy);
610 }
611
612 if let Some(proxy_no_verify) = all_languages
613 .edit_predictions
614 .as_ref()
615 .and_then(|settings| settings.copilot.as_ref()?.proxy_no_verify)
616 {
617 self.edit_predictions.copilot.proxy_no_verify = Some(proxy_no_verify);
618 }
619
620 if let Some(enterprise_uri) = all_languages
621 .edit_predictions
622 .as_ref()
623 .and_then(|settings| settings.copilot.as_ref()?.enterprise_uri.clone())
624 {
625 self.edit_predictions.copilot.enterprise_uri = Some(enterprise_uri);
626 }
627
628 // A user's global settings override the default global settings and
629 // all default language-specific settings.
630 merge_settings(&mut self.defaults, &all_languages.defaults);
631 for language_settings in self.languages.values_mut() {
632 merge_settings(language_settings, &all_languages.defaults);
633 }
634
635 // A user's language-specific settings override default language-specific settings.
636 for (language_name, user_language_settings) in &all_languages.languages.0 {
637 merge_settings(
638 self.languages
639 .entry(LanguageName(language_name.clone()))
640 .or_insert_with(|| self.defaults.clone()),
641 user_language_settings,
642 );
643 }
644
645 for (language, patterns) in &all_languages.file_types {
646 let mut builder = GlobSetBuilder::new();
647
648 let default_value = self.file_globs.get(&language.clone());
649
650 // Merge the default value with the user's value.
651 if let Some(patterns) = default_value {
652 for pattern in patterns {
653 if let Some(glob) = Glob::new(pattern).log_err() {
654 builder.add(glob);
655 }
656 }
657 }
658
659 for pattern in patterns {
660 if let Some(glob) = Glob::new(pattern).log_err() {
661 builder.add(glob);
662 }
663 }
664
665 self.file_globs
666 .entry(language.clone())
667 .or_default()
668 .extend(patterns.clone());
669
670 if let Some(matcher) = builder.build().log_err() {
671 self.file_types.insert(language.clone(), matcher);
672 }
673 }
674 }
675
676 fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
677 let d = &mut current.project.all_languages.defaults;
678 if let Some(size) = vscode
679 .read_value("editor.tabSize")
680 .and_then(|v| v.as_u64())
681 .and_then(|n| NonZeroU32::new(n as u32))
682 {
683 d.tab_size = Some(size);
684 }
685 if let Some(v) = vscode.read_bool("editor.insertSpaces") {
686 d.hard_tabs = Some(!v);
687 }
688
689 vscode.enum_setting("editor.wordWrap", &mut d.soft_wrap, |s| match s {
690 "on" => Some(SoftWrap::EditorWidth),
691 "wordWrapColumn" => Some(SoftWrap::PreferLine),
692 "bounded" => Some(SoftWrap::Bounded),
693 "off" => Some(SoftWrap::None),
694 _ => None,
695 });
696 vscode.u32_setting("editor.wordWrapColumn", &mut d.preferred_line_length);
697
698 if let Some(arr) = vscode
699 .read_value("editor.rulers")
700 .and_then(|v| v.as_array())
701 .map(|v| v.iter().map(|n| n.as_u64().map(|n| n as usize)).collect())
702 {
703 d.wrap_guides = arr;
704 }
705 if let Some(b) = vscode.read_bool("editor.guides.indentation") {
706 if let Some(guide_settings) = d.indent_guides.as_mut() {
707 guide_settings.enabled = b;
708 } else {
709 d.indent_guides = Some(IndentGuideSettings {
710 enabled: b,
711 ..Default::default()
712 });
713 }
714 }
715
716 if let Some(b) = vscode.read_bool("editor.guides.formatOnSave") {
717 d.format_on_save = Some(if b {
718 FormatOnSave::On
719 } else {
720 FormatOnSave::Off
721 });
722 }
723 vscode.bool_setting(
724 "editor.trimAutoWhitespace",
725 &mut d.remove_trailing_whitespace_on_save,
726 );
727 vscode.bool_setting(
728 "files.insertFinalNewline",
729 &mut d.ensure_final_newline_on_save,
730 );
731 vscode.bool_setting("editor.inlineSuggest.enabled", &mut d.show_edit_predictions);
732 vscode.enum_setting("editor.renderWhitespace", &mut d.show_whitespaces, |s| {
733 Some(match s {
734 "boundary" => ShowWhitespaceSetting::Boundary,
735 "trailing" => ShowWhitespaceSetting::Trailing,
736 "selection" => ShowWhitespaceSetting::Selection,
737 "all" => ShowWhitespaceSetting::All,
738 _ => ShowWhitespaceSetting::None,
739 })
740 });
741 vscode.enum_setting(
742 "editor.autoSurround",
743 &mut d.use_auto_surround,
744 |s| match s {
745 "languageDefined" | "quotes" | "brackets" => Some(true),
746 "never" => Some(false),
747 _ => None,
748 },
749 );
750 vscode.bool_setting("editor.formatOnType", &mut d.use_on_type_format);
751 vscode.bool_setting("editor.linkedEditing", &mut d.linked_edits);
752 vscode.bool_setting("editor.formatOnPaste", &mut d.auto_indent_on_paste);
753 vscode.bool_setting(
754 "editor.suggestOnTriggerCharacters",
755 &mut d.show_completions_on_input,
756 );
757 if let Some(b) = vscode.read_bool("editor.suggest.showWords") {
758 let mode = if b {
759 WordsCompletionMode::Enabled
760 } else {
761 WordsCompletionMode::Disabled
762 };
763 if let Some(completion_settings) = d.completions.as_mut() {
764 completion_settings.words = mode;
765 } else {
766 d.completions = Some(CompletionSettings {
767 words: mode,
768 words_min_length: 3,
769 lsp: true,
770 lsp_fetch_timeout_ms: 0,
771 lsp_insert_mode: LspInsertMode::ReplaceSuffix,
772 });
773 }
774 }
775 // TODO: pull ^ out into helper and reuse for per-language settings
776
777 // vscodes file association map is inverted from ours, so we flip the mapping before merging
778 let mut associations: HashMap<Arc<str>, Vec<String>> = HashMap::default();
779 if let Some(map) = vscode
780 .read_value("files.associations")
781 .and_then(|v| v.as_object())
782 {
783 for (k, v) in map {
784 let Some(v) = v.as_str() else { continue };
785 associations.entry(v.into()).or_default().push(k.clone());
786 }
787 }
788
789 // TODO: do we want to merge imported globs per filetype? for now we'll just replace
790 current
791 .project
792 .all_languages
793 .file_types
794 .extend(associations);
795
796 // cursor global ignore list applies to cursor-tab, so transfer it to edit_predictions.disabled_globs
797 if let Some(disabled_globs) = vscode
798 .read_value("cursor.general.globalCursorIgnoreList")
799 .and_then(|v| v.as_array())
800 {
801 current
802 .project
803 .all_languages
804 .edit_predictions
805 .get_or_insert_default()
806 .disabled_globs
807 .get_or_insert_default()
808 .extend(
809 disabled_globs
810 .iter()
811 .filter_map(|glob| glob.as_str())
812 .map(|s| s.to_string()),
813 );
814 }
815 }
816}
817
818fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent) {
819 settings.tab_size.merge_from(&src.tab_size);
820 settings.tab_size = settings
821 .tab_size
822 .clamp(NonZeroU32::new(1).unwrap(), NonZeroU32::new(16).unwrap());
823
824 settings.hard_tabs.merge_from(&src.hard_tabs);
825 settings.soft_wrap.merge_from(&src.soft_wrap);
826 settings.use_autoclose.merge_from(&src.use_autoclose);
827 settings
828 .use_auto_surround
829 .merge_from(&src.use_auto_surround);
830 settings
831 .use_on_type_format
832 .merge_from(&src.use_on_type_format);
833 settings.auto_indent.merge_from(&src.auto_indent);
834 settings
835 .auto_indent_on_paste
836 .merge_from(&src.auto_indent_on_paste);
837 settings
838 .always_treat_brackets_as_autoclosed
839 .merge_from(&src.always_treat_brackets_as_autoclosed);
840 settings.show_wrap_guides.merge_from(&src.show_wrap_guides);
841 settings.wrap_guides.merge_from(&src.wrap_guides);
842 settings.indent_guides.merge_from(&src.indent_guides);
843 settings
844 .code_actions_on_format
845 .merge_from(&src.code_actions_on_format.clone());
846 settings.linked_edits.merge_from(&src.linked_edits);
847 settings.tasks.merge_from(&src.tasks.clone());
848
849 settings
850 .preferred_line_length
851 .merge_from(&src.preferred_line_length);
852 settings.formatter.merge_from(&src.formatter.clone());
853 settings.prettier.merge_from(&src.prettier.clone());
854 settings
855 .jsx_tag_auto_close
856 .merge_from(&src.jsx_tag_auto_close.clone());
857 settings
858 .format_on_save
859 .merge_from(&src.format_on_save.clone());
860 settings
861 .remove_trailing_whitespace_on_save
862 .merge_from(&src.remove_trailing_whitespace_on_save);
863 settings
864 .ensure_final_newline_on_save
865 .merge_from(&src.ensure_final_newline_on_save);
866 settings
867 .enable_language_server
868 .merge_from(&src.enable_language_server);
869 settings
870 .language_servers
871 .merge_from(&src.language_servers.clone());
872 settings.allow_rewrap.merge_from(&src.allow_rewrap);
873 settings
874 .show_edit_predictions
875 .merge_from(&src.show_edit_predictions);
876 settings
877 .edit_predictions_disabled_in
878 .merge_from(&src.edit_predictions_disabled_in.clone());
879 settings.show_whitespaces.merge_from(&src.show_whitespaces);
880 settings
881 .whitespace_map
882 .merge_from(&src.whitespace_map.clone());
883 settings
884 .extend_comment_on_newline
885 .merge_from(&src.extend_comment_on_newline);
886 if let Some(inlay_hints) = &src.inlay_hints {
887 settings
888 .inlay_hints
889 .enabled
890 .merge_from(&inlay_hints.enabled);
891 settings
892 .inlay_hints
893 .show_value_hints
894 .merge_from(&inlay_hints.show_value_hints);
895 settings
896 .inlay_hints
897 .show_type_hints
898 .merge_from(&inlay_hints.show_type_hints);
899 settings
900 .inlay_hints
901 .show_parameter_hints
902 .merge_from(&inlay_hints.show_parameter_hints);
903 settings
904 .inlay_hints
905 .show_other_hints
906 .merge_from(&inlay_hints.show_other_hints);
907 settings
908 .inlay_hints
909 .show_background
910 .merge_from(&inlay_hints.show_background);
911 settings
912 .inlay_hints
913 .edit_debounce_ms
914 .merge_from(&inlay_hints.edit_debounce_ms);
915 settings
916 .inlay_hints
917 .scroll_debounce_ms
918 .merge_from(&inlay_hints.scroll_debounce_ms);
919 if let Some(toggle_on_modifiers_press) = &inlay_hints.toggle_on_modifiers_press {
920 settings.inlay_hints.toggle_on_modifiers_press = Some(*toggle_on_modifiers_press);
921 }
922 }
923 settings
924 .show_completions_on_input
925 .merge_from(&src.show_completions_on_input);
926 settings
927 .show_completion_documentation
928 .merge_from(&src.show_completion_documentation);
929 settings.completions.merge_from(&src.completions.clone());
930}
931
932/// Allows to enable/disable formatting with Prettier
933/// and configure default Prettier, used when no project-level Prettier installation is found.
934/// Prettier formatting is disabled by default.
935#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi)]
936pub struct PrettierSettings {
937 /// Enables or disables formatting with Prettier for a given language.
938 #[serde(default)]
939 pub allowed: bool,
940
941 /// Forces Prettier integration to use a specific parser name when formatting files with the language.
942 #[serde(default)]
943 pub parser: Option<String>,
944
945 /// Forces Prettier integration to use specific plugins when formatting files with the language.
946 /// The default Prettier will be installed with these plugins.
947 #[serde(default)]
948 #[settings_ui(skip)]
949 pub plugins: HashSet<String>,
950
951 /// Default Prettier options, in the format as in package.json section for Prettier.
952 /// If project installs Prettier via its package.json, these options will be ignored.
953 #[serde(flatten)]
954 #[settings_ui(skip)]
955 pub options: HashMap<String, serde_json::Value>,
956}
957
958#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi)]
959pub struct JsxTagAutoCloseSettings {
960 /// Enables or disables auto-closing of JSX tags.
961 #[serde(default)]
962 pub enabled: bool,
963}
964
965#[cfg(test)]
966mod tests {
967 use super::*;
968 use gpui::TestAppContext;
969
970 #[gpui::test]
971 fn test_edit_predictions_enabled_for_file(cx: &mut TestAppContext) {
972 use crate::TestFile;
973 use std::path::PathBuf;
974
975 let cx = cx.app.borrow_mut();
976
977 let build_settings = |globs: &[&str]| -> EditPredictionSettings {
978 EditPredictionSettings {
979 disabled_globs: globs
980 .iter()
981 .map(|glob_str| {
982 #[cfg(windows)]
983 let glob_str = {
984 let mut g = String::new();
985
986 if glob_str.starts_with('/') {
987 g.push_str("C:");
988 }
989
990 g.push_str(&glob_str.replace('/', "\\"));
991 g
992 };
993 #[cfg(windows)]
994 let glob_str = glob_str.as_str();
995 let expanded_glob_str = shellexpand::tilde(glob_str).into_owned();
996 DisabledGlob {
997 matcher: globset::Glob::new(&expanded_glob_str)
998 .unwrap()
999 .compile_matcher(),
1000 is_absolute: Path::new(&expanded_glob_str).is_absolute(),
1001 }
1002 })
1003 .collect(),
1004 ..Default::default()
1005 }
1006 };
1007
1008 const WORKTREE_NAME: &str = "project";
1009 let make_test_file = |segments: &[&str]| -> Arc<dyn File> {
1010 let mut path_buf = PathBuf::new();
1011 path_buf.extend(segments);
1012
1013 Arc::new(TestFile {
1014 path: path_buf.as_path().into(),
1015 root_name: WORKTREE_NAME.to_string(),
1016 local_root: Some(PathBuf::from(if cfg!(windows) {
1017 "C:\\absolute\\"
1018 } else {
1019 "/absolute/"
1020 })),
1021 })
1022 };
1023
1024 let test_file = make_test_file(&["src", "test", "file.rs"]);
1025
1026 // Test relative globs
1027 let settings = build_settings(&["*.rs"]);
1028 assert!(!settings.enabled_for_file(&test_file, &cx));
1029 let settings = build_settings(&["*.txt"]);
1030 assert!(settings.enabled_for_file(&test_file, &cx));
1031
1032 // Test absolute globs
1033 let settings = build_settings(&["/absolute/**/*.rs"]);
1034 assert!(!settings.enabled_for_file(&test_file, &cx));
1035 let settings = build_settings(&["/other/**/*.rs"]);
1036 assert!(settings.enabled_for_file(&test_file, &cx));
1037
1038 // Test exact path match relative
1039 let settings = build_settings(&["src/test/file.rs"]);
1040 assert!(!settings.enabled_for_file(&test_file, &cx));
1041 let settings = build_settings(&["src/test/otherfile.rs"]);
1042 assert!(settings.enabled_for_file(&test_file, &cx));
1043
1044 // Test exact path match absolute
1045 let settings = build_settings(&[&format!("/absolute/{}/src/test/file.rs", WORKTREE_NAME)]);
1046 assert!(!settings.enabled_for_file(&test_file, &cx));
1047 let settings = build_settings(&["/other/test/otherfile.rs"]);
1048 assert!(settings.enabled_for_file(&test_file, &cx));
1049
1050 // Test * glob
1051 let settings = build_settings(&["*"]);
1052 assert!(!settings.enabled_for_file(&test_file, &cx));
1053 let settings = build_settings(&["*.txt"]);
1054 assert!(settings.enabled_for_file(&test_file, &cx));
1055
1056 // Test **/* glob
1057 let settings = build_settings(&["**/*"]);
1058 assert!(!settings.enabled_for_file(&test_file, &cx));
1059 let settings = build_settings(&["other/**/*"]);
1060 assert!(settings.enabled_for_file(&test_file, &cx));
1061
1062 // Test directory/** glob
1063 let settings = build_settings(&["src/**"]);
1064 assert!(!settings.enabled_for_file(&test_file, &cx));
1065
1066 let test_file_root: Arc<dyn File> = Arc::new(TestFile {
1067 path: PathBuf::from("file.rs").as_path().into(),
1068 root_name: WORKTREE_NAME.to_string(),
1069 local_root: Some(PathBuf::from("/absolute/")),
1070 });
1071 assert!(settings.enabled_for_file(&test_file_root, &cx));
1072
1073 let settings = build_settings(&["other/**"]);
1074 assert!(settings.enabled_for_file(&test_file, &cx));
1075
1076 // Test **/directory/* glob
1077 let settings = build_settings(&["**/test/*"]);
1078 assert!(!settings.enabled_for_file(&test_file, &cx));
1079 let settings = build_settings(&["**/other/*"]);
1080 assert!(settings.enabled_for_file(&test_file, &cx));
1081
1082 // Test multiple globs
1083 let settings = build_settings(&["*.rs", "*.txt", "src/**"]);
1084 assert!(!settings.enabled_for_file(&test_file, &cx));
1085 let settings = build_settings(&["*.txt", "*.md", "other/**"]);
1086 assert!(settings.enabled_for_file(&test_file, &cx));
1087
1088 // Test dot files
1089 let dot_file = make_test_file(&[".config", "settings.json"]);
1090 let settings = build_settings(&[".*/**"]);
1091 assert!(!settings.enabled_for_file(&dot_file, &cx));
1092
1093 let dot_env_file = make_test_file(&[".env"]);
1094 let settings = build_settings(&[".env"]);
1095 assert!(!settings.enabled_for_file(&dot_env_file, &cx));
1096
1097 // Test tilde expansion
1098 let home = shellexpand::tilde("~").into_owned();
1099 let home_file = make_test_file(&[&home, "test.rs"]);
1100 let settings = build_settings(&["~/test.rs"]);
1101 assert!(!settings.enabled_for_file(&home_file, &cx));
1102 }
1103
1104 #[test]
1105 fn test_resolve_language_servers() {
1106 fn language_server_names(names: &[&str]) -> Vec<LanguageServerName> {
1107 names
1108 .iter()
1109 .copied()
1110 .map(|name| LanguageServerName(name.to_string().into()))
1111 .collect::<Vec<_>>()
1112 }
1113
1114 let available_language_servers = language_server_names(&[
1115 "typescript-language-server",
1116 "biome",
1117 "deno",
1118 "eslint",
1119 "tailwind",
1120 ]);
1121
1122 // A value of just `["..."]` is the same as taking all of the available language servers.
1123 assert_eq!(
1124 LanguageSettings::resolve_language_servers(
1125 &[LanguageSettings::REST_OF_LANGUAGE_SERVERS.into()],
1126 &available_language_servers,
1127 ),
1128 available_language_servers
1129 );
1130
1131 // Referencing one of the available language servers will change its order.
1132 assert_eq!(
1133 LanguageSettings::resolve_language_servers(
1134 &[
1135 "biome".into(),
1136 LanguageSettings::REST_OF_LANGUAGE_SERVERS.into(),
1137 "deno".into()
1138 ],
1139 &available_language_servers
1140 ),
1141 language_server_names(&[
1142 "biome",
1143 "typescript-language-server",
1144 "eslint",
1145 "tailwind",
1146 "deno",
1147 ])
1148 );
1149
1150 // Negating an available language server removes it from the list.
1151 assert_eq!(
1152 LanguageSettings::resolve_language_servers(
1153 &[
1154 "deno".into(),
1155 "!typescript-language-server".into(),
1156 "!biome".into(),
1157 LanguageSettings::REST_OF_LANGUAGE_SERVERS.into()
1158 ],
1159 &available_language_servers
1160 ),
1161 language_server_names(&["deno", "eslint", "tailwind"])
1162 );
1163
1164 // Adding a language server not in the list of available language servers adds it to the list.
1165 assert_eq!(
1166 LanguageSettings::resolve_language_servers(
1167 &[
1168 "my-cool-language-server".into(),
1169 LanguageSettings::REST_OF_LANGUAGE_SERVERS.into()
1170 ],
1171 &available_language_servers
1172 ),
1173 language_server_names(&[
1174 "my-cool-language-server",
1175 "typescript-language-server",
1176 "biome",
1177 "deno",
1178 "eslint",
1179 "tailwind",
1180 ])
1181 );
1182 }
1183}