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