Skip to repository content1513 lines · 61.1 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:51:04.433Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
language_settings.rs
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::{
10 EndOfLine, FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs,
11 },
12};
13use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
14use gpui::{App, Modifiers, SharedString};
15use itertools::Itertools;
16use settings::{DocumentFoldingRanges, DocumentSymbols, IntoGpui, SemanticTokens};
17
18pub use settings::{
19 AutoIndentMode, CompletionSettingsContent, ConfiguredLanguageServer,
20 EditPredictionDataCollectionChoice, EditPredictionPromptFormatContent, EditPredictionProvider,
21 EditPredictionsMode, FormatOnSave, Formatter, FormatterList, InlayHintKind,
22 LanguageSettingsContent, LineEndingSetting, LspInsertMode, REST_OF_LANGUAGE_SERVERS,
23 RewrapBehavior, ShowWhitespaceSetting, SoftWrap, WordsCompletionMode,
24};
25use settings::{RegisterSetting, Settings, SettingsLocation, SettingsStore, merge_from::MergeFrom};
26use shellexpand;
27use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc};
28use text::ToOffset;
29
30/// Returns the settings for all languages from the provided file.
31pub fn all_language_settings<'a>(
32 file: Option<&'a Arc<dyn File>>,
33 cx: &'a App,
34) -> &'a AllLanguageSettings {
35 let location = file.map(|f| SettingsLocation {
36 worktree_id: f.worktree_id(cx),
37 path: f.path().as_ref(),
38 });
39 AllLanguageSettings::get(location, cx)
40}
41
42/// The settings for all languages.
43#[derive(Debug, Clone, RegisterSetting)]
44pub struct AllLanguageSettings {
45 /// The edit prediction settings.
46 pub edit_predictions: EditPredictionSettings,
47 pub defaults: LanguageSettings,
48 languages: HashMap<LanguageName, LanguageSettings>,
49 pub file_types: FxHashMap<Arc<str>, (GlobSet, Vec<String>)>,
50}
51
52#[derive(Debug, Clone, PartialEq)]
53pub struct WhitespaceMap {
54 pub space: SharedString,
55 pub tab: SharedString,
56}
57
58/// The settings for a particular language.
59#[derive(Debug, Clone, PartialEq)]
60pub struct LanguageSettings {
61 /// How many columns a tab should occupy.
62 pub tab_size: NonZeroU32,
63 /// Whether to indent lines using tab characters, as opposed to multiple
64 /// spaces.
65 pub hard_tabs: bool,
66 /// How to soft-wrap long lines of text.
67 pub soft_wrap: settings::SoftWrap,
68 /// The column at which to soft-wrap lines, for buffers where soft-wrap
69 /// is enabled.
70 pub preferred_line_length: u32,
71 /// Whether to show wrap guides (vertical rulers) in the editor.
72 /// Setting this to true will show a guide at the 'preferred_line_length' value
73 /// if softwrap is set to 'preferred_line_length', and will show any
74 /// additional guides as specified by the 'wrap_guides' setting.
75 pub show_wrap_guides: bool,
76 /// Character counts at which to show wrap guides (vertical rulers) in the editor.
77 pub wrap_guides: Vec<usize>,
78 /// Indent guide related settings.
79 pub indent_guides: IndentGuideSettings,
80 /// Whether or not to perform a buffer format before saving.
81 pub format_on_save: FormatOnSave,
82 /// Whether or not to remove any trailing whitespace from lines of a buffer
83 /// before saving it.
84 pub remove_trailing_whitespace_on_save: bool,
85 /// Whether or not to ensure there's a single newline at the end of a buffer
86 /// when saving it.
87 pub ensure_final_newline_on_save: bool,
88 /// How line endings are initialized for new files and normalized during
89 /// format and save.
90 pub line_ending: LineEndingSetting,
91 /// How to perform a buffer format.
92 pub formatter: settings::FormatterList,
93 /// Zed's Prettier integration settings.
94 pub prettier: PrettierSettings,
95 /// Whether to automatically close JSX tags.
96 pub jsx_tag_auto_close: bool,
97 /// Whether to use language servers to provide code intelligence.
98 pub enable_language_server: bool,
99 /// The list of language servers to use (or disable) for this language.
100 ///
101 /// This array should consist of language server IDs, as well as the following
102 /// special tokens:
103 /// - `"!<language_server_id>"` - A language server ID prefixed with a `!` will be disabled.
104 /// - `"..."` - A placeholder to refer to the **rest** of the registered language servers for this language.
105 pub language_servers: Vec<ConfiguredLanguageServer>,
106 /// Controls how semantic tokens from language servers are used for syntax highlighting.
107 pub semantic_tokens: SemanticTokens,
108 /// Controls whether folding ranges from language servers are used instead of
109 /// tree-sitter and indent-based folding.
110 pub document_folding_ranges: DocumentFoldingRanges,
111 /// Controls the source of document symbols used for outlines and breadcrumbs.
112 pub document_symbols: DocumentSymbols,
113 /// Controls where the `editor::Rewrap` action is allowed for this language.
114 ///
115 /// Note: This setting has no effect in Vim mode, as rewrap is already
116 /// allowed everywhere.
117 pub allow_rewrap: RewrapBehavior,
118 /// Controls whether edit predictions are shown immediately (true)
119 /// or manually by triggering `editor::ShowEditPrediction` (false).
120 pub show_edit_predictions: bool,
121 /// Controls whether edit predictions are shown in the given language
122 /// scopes.
123 pub edit_predictions_disabled_in: Vec<String>,
124 /// Whether to show tabs and spaces in the editor.
125 pub show_whitespaces: settings::ShowWhitespaceSetting,
126 /// Visible characters used to render whitespace when show_whitespaces is enabled.
127 pub whitespace_map: WhitespaceMap,
128 /// Whether to start a new line with a comment when a previous line is a comment as well.
129 pub extend_comment_on_newline: bool,
130 /// Whether to continue markdown lists when pressing enter.
131 pub extend_list_on_newline: bool,
132 /// Whether to indent list items when pressing tab after a list marker.
133 pub indent_list_on_tab: bool,
134 /// Inlay hint related settings.
135 pub inlay_hints: InlayHintSettings,
136 /// Whether to automatically close brackets.
137 pub use_autoclose: bool,
138 /// Whether to automatically surround text with brackets.
139 pub use_auto_surround: bool,
140 /// Whether to use additional LSP queries to format (and amend) the code after
141 /// every "trigger" symbol input, defined by LSP server capabilities.
142 pub use_on_type_format: bool,
143 /// Controls automatic indentation behavior when typing.
144 pub auto_indent: AutoIndentMode,
145 /// Whether indentation of pasted content should be adjusted based on the context.
146 pub auto_indent_on_paste: bool,
147 /// Controls how the editor handles the autoclosed characters.
148 pub always_treat_brackets_as_autoclosed: bool,
149 /// Which code actions to run on save
150 pub code_actions_on_format: HashMap<String, bool>,
151 /// Whether to perform linked edits
152 pub linked_edits: bool,
153 /// Task configuration for this language.
154 pub tasks: LanguageTaskSettings,
155 /// Whether to pop the completions menu while typing in an editor without
156 /// explicitly requesting it.
157 pub show_completions_on_input: bool,
158 /// Whether to display inline and alongside documentation for items in the
159 /// completions menu.
160 pub show_completion_documentation: bool,
161 /// Completion settings for this language.
162 pub completions: CompletionSettings,
163 /// Preferred debuggers for this language.
164 pub debuggers: Vec<String>,
165 /// Whether to enable word diff highlighting in the editor.
166 ///
167 /// When enabled, changed words within modified lines are highlighted
168 /// to show exactly what changed.
169 ///
170 /// Default: `true`
171 pub word_diff_enabled: bool,
172 /// Whether to use tree-sitter bracket queries to detect and colorize the brackets in the editor.
173 pub colorize_brackets: bool,
174}
175
176#[derive(Debug, Clone, PartialEq)]
177pub struct CompletionSettings {
178 /// Controls how words are completed.
179 /// For large documents, not all words may be fetched for completion.
180 ///
181 /// Default: `fallback`
182 pub words: WordsCompletionMode,
183 /// How many characters has to be in the completions query to automatically show the words-based completions.
184 /// Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command.
185 ///
186 /// Default: 3
187 pub words_min_length: usize,
188 /// Whether to fetch LSP completions or not.
189 ///
190 /// Default: true
191 pub lsp: bool,
192 /// When fetching LSP completions, determines how long to wait for a response of a particular server.
193 /// When set to 0, waits indefinitely.
194 ///
195 /// Default: 0
196 pub lsp_fetch_timeout_ms: u64,
197 /// Controls how LSP completions are inserted.
198 ///
199 /// Default: "replace_suffix"
200 pub lsp_insert_mode: LspInsertMode,
201}
202
203/// The settings for indent guides.
204#[derive(Debug, Clone, PartialEq)]
205pub struct IndentGuideSettings {
206 /// Whether to display indent guides in the editor.
207 ///
208 /// Default: true
209 pub enabled: bool,
210 /// The width of the indent guides in pixels, between 1 and 10.
211 ///
212 /// Default: 1
213 pub line_width: u32,
214 /// The width of the active indent guide in pixels, between 1 and 10.
215 ///
216 /// Default: 1
217 pub active_line_width: u32,
218 /// Determines how indent guides are colored.
219 ///
220 /// Default: Fixed
221 pub coloring: settings::IndentGuideColoring,
222 /// Determines how indent guide backgrounds are colored.
223 ///
224 /// Default: Disabled
225 pub background_coloring: settings::IndentGuideBackgroundColoring,
226}
227
228impl IndentGuideSettings {
229 /// Returns the clamped line width in pixels for an indent guide based on
230 /// whether it is active, or `None` when line coloring is disabled.
231 pub fn visible_line_width(&self, active: bool) -> Option<u32> {
232 if self.coloring == settings::IndentGuideColoring::Disabled {
233 return None;
234 }
235 let width = if active {
236 self.active_line_width
237 } else {
238 self.line_width
239 };
240 Some(width.clamp(1, 10))
241 }
242}
243
244#[derive(Debug, Clone, PartialEq)]
245pub struct LanguageTaskSettings {
246 /// Extra task variables to set for a particular language.
247 pub variables: HashMap<String, String>,
248 pub enabled: bool,
249 /// Use LSP tasks over Zed language extension ones.
250 /// If no LSP tasks are returned due to error/timeout or regular execution,
251 /// Zed language extension tasks will be used instead.
252 ///
253 /// Other Zed tasks will still be shown:
254 /// * Zed task from either of the task config file
255 /// * Zed task from history (e.g. one-off task was spawned before)
256 pub prefer_lsp: bool,
257}
258
259/// Allows to enable/disable formatting with Prettier
260/// and configure default Prettier, used when no project-level Prettier installation is found.
261/// Prettier formatting is disabled by default.
262#[derive(Debug, Clone, PartialEq)]
263pub struct PrettierSettings {
264 /// Enables or disables formatting with Prettier for a given language.
265 pub allowed: bool,
266
267 /// Forces Prettier integration to use a specific parser name when formatting files with the language.
268 pub parser: Option<String>,
269
270 /// Forces Prettier integration to use specific plugins when formatting files with the language.
271 /// The default Prettier will be installed with these plugins.
272 pub plugins: HashSet<String>,
273
274 /// Default Prettier options, in the format as in package.json section for Prettier.
275 /// If project installs Prettier via its package.json, these options will be ignored.
276 pub options: HashMap<String, serde_json::Value>,
277}
278
279impl LanguageSettings {
280 pub fn for_buffer<'a>(buffer: &'a Buffer, cx: &'a App) -> Cow<'a, LanguageSettings> {
281 Self::resolve(Some(buffer), None, cx)
282 }
283
284 pub fn for_buffer_at<'a, D: ToOffset>(
285 buffer: &'a Buffer,
286 position: D,
287 cx: &'a App,
288 ) -> Cow<'a, LanguageSettings> {
289 let language = buffer.language_at(position);
290 Self::resolve(Some(buffer), language.map(|l| l.name()).as_ref(), cx)
291 }
292
293 pub fn for_buffer_snapshot<'a>(
294 buffer: &'a BufferSnapshot,
295 offset: Option<usize>,
296 cx: &'a App,
297 ) -> Cow<'a, LanguageSettings> {
298 let location = buffer.file().map(|f| SettingsLocation {
299 worktree_id: f.worktree_id(cx),
300 path: f.path().as_ref(),
301 });
302
303 let language = if let Some(offset) = offset {
304 buffer.language_at(offset)
305 } else {
306 buffer.language()
307 };
308
309 let mut settings = AllLanguageSettings::get(location, cx).language(
310 location,
311 language.map(|l| l.name()).as_ref(),
312 cx,
313 );
314
315 if let Some(modeline) = buffer.modeline() {
316 merge_with_modeline(settings.to_mut(), modeline);
317 }
318
319 settings
320 }
321
322 pub fn resolve<'a>(
323 buffer: Option<&'a Buffer>,
324 override_language: Option<&LanguageName>,
325 cx: &'a App,
326 ) -> Cow<'a, LanguageSettings> {
327 let Some(buffer) = buffer else {
328 return AllLanguageSettings::get(None, cx).language(None, override_language, cx);
329 };
330 let location = buffer.file().map(|f| SettingsLocation {
331 worktree_id: f.worktree_id(cx),
332 path: f.path().as_ref(),
333 });
334 let all = AllLanguageSettings::get(location, cx);
335 let mut settings = if override_language.is_none() {
336 all.language(location, buffer.language().map(|l| l.name()).as_ref(), cx)
337 } else {
338 all.language(location, override_language, cx)
339 };
340
341 if let Some(modeline) = buffer.modeline() {
342 merge_with_modeline(settings.to_mut(), modeline);
343 }
344
345 settings
346 }
347
348 /// Returns the customized list of language servers from the list of
349 /// available language servers.
350 pub fn customized_language_servers(
351 &self,
352 available_language_servers: &[LanguageServerName],
353 ) -> Vec<LanguageServerName> {
354 Self::resolve_language_servers(&self.language_servers, available_language_servers)
355 }
356
357 pub(crate) fn resolve_language_servers(
358 configured_language_servers: &[ConfiguredLanguageServer],
359 available_language_servers: &[LanguageServerName],
360 ) -> Vec<LanguageServerName> {
361 let (disabled_language_servers, enabled_language_servers): (
362 Vec<LanguageServerName>,
363 Vec<LanguageServerName>,
364 ) = configured_language_servers
365 .iter()
366 .partition_map(|language_server| {
367 let name = LanguageServerName(language_server.name.clone().into());
368 if language_server.disabled {
369 itertools::Either::Left(name)
370 } else {
371 itertools::Either::Right(name)
372 }
373 });
374
375 let rest = available_language_servers
376 .iter()
377 .filter(|&available_language_server| {
378 !disabled_language_servers.contains(available_language_server)
379 && !enabled_language_servers.contains(available_language_server)
380 })
381 .cloned()
382 .collect::<Vec<_>>();
383
384 enabled_language_servers
385 .into_iter()
386 .flat_map(|language_server| {
387 if language_server.0.as_ref() == REST_OF_LANGUAGE_SERVERS {
388 rest.clone()
389 } else {
390 vec![language_server]
391 }
392 })
393 .collect::<Vec<_>>()
394 }
395}
396
397// The settings for inlay hints.
398#[derive(Copy, Clone, Debug, PartialEq, Eq)]
399pub struct InlayHintSettings {
400 /// Global switch to toggle hints on and off.
401 ///
402 /// Default: false
403 pub enabled: bool,
404 /// Global switch to toggle inline values on and off when debugging.
405 ///
406 /// Default: true
407 pub show_value_hints: bool,
408 /// Whether type hints should be shown.
409 ///
410 /// Default: true
411 pub show_type_hints: bool,
412 /// Whether parameter hints should be shown.
413 ///
414 /// Default: true
415 pub show_parameter_hints: bool,
416 /// Whether other hints should be shown.
417 ///
418 /// Default: true
419 pub show_other_hints: bool,
420 /// Whether to show a background for inlay hints.
421 ///
422 /// If set to `true`, the background will use the `hint.background` color
423 /// from the current theme.
424 ///
425 /// Default: false
426 pub show_background: bool,
427 /// Whether or not to debounce inlay hints updates after buffer edits.
428 ///
429 /// Set to 0 to disable debouncing.
430 ///
431 /// Default: 700
432 pub edit_debounce_ms: u64,
433 /// Whether or not to debounce inlay hints updates after buffer scrolls.
434 ///
435 /// Set to 0 to disable debouncing.
436 ///
437 /// Default: 50
438 pub scroll_debounce_ms: u64,
439 /// Toggles inlay hints (hides or shows) when the user presses the modifiers specified.
440 /// If only a subset of the modifiers specified is pressed, hints are not toggled.
441 /// If no modifiers are specified, this is equivalent to `None`.
442 ///
443 /// Default: None
444 pub toggle_on_modifiers_press: Option<Modifiers>,
445}
446
447impl InlayHintSettings {
448 /// Returns the kinds of inlay hints that are enabled based on the settings.
449 pub fn enabled_inlay_hint_kinds(&self) -> HashSet<Option<InlayHintKind>> {
450 let mut kinds = HashSet::default();
451 if self.show_type_hints {
452 kinds.insert(Some(InlayHintKind::Type));
453 }
454 if self.show_parameter_hints {
455 kinds.insert(Some(InlayHintKind::Parameter));
456 }
457 if self.show_other_hints {
458 kinds.insert(None);
459 }
460 kinds
461 }
462}
463
464/// The settings for edit predictions, such as [GitHub Copilot](https://github.com/features/copilot).
465#[derive(Clone, Debug, Default)]
466pub struct EditPredictionSettings {
467 /// The provider that supplies edit predictions.
468 pub provider: settings::EditPredictionProvider,
469 /// A list of globs representing files that edit predictions should be disabled for.
470 /// This list adds to a pre-existing, sensible default set of globs.
471 /// Any additional ones you add are combined with them.
472 pub disabled_globs: Vec<DisabledGlob>,
473 /// Configures how edit predictions are displayed in the buffer.
474 pub mode: settings::EditPredictionsMode,
475 /// Settings specific to GitHub Copilot.
476 pub copilot: CopilotSettings,
477 /// Settings specific to Codestral.
478 pub codestral: CodestralSettings,
479 /// Settings specific to Ollama.
480 pub ollama: Option<OpenAiCompatibleEditPredictionSettings>,
481 pub open_ai_compatible_api: Option<OpenAiCompatibleEditPredictionSettings>,
482 /// Controls whether training data collection is enabled.
483 ///
484 /// `Default` means the value stored in the legacy KV store is used as a fallback,
485 /// preserving existing users' choices without a migration.
486 pub allow_data_collection: EditPredictionDataCollectionChoice,
487}
488
489impl EditPredictionSettings {
490 /// Returns whether edit predictions are enabled for the given path.
491 pub fn enabled_for_file(&self, file: &Arc<dyn File>, cx: &App) -> bool {
492 !self.disabled_globs.iter().any(|glob| {
493 if glob.is_absolute {
494 file.as_local()
495 .is_some_and(|local| glob.matcher.is_match(local.abs_path(cx)))
496 } else {
497 glob.matcher.is_match(file.path().as_std_path())
498 }
499 })
500 }
501}
502
503#[derive(Clone, Debug)]
504pub struct DisabledGlob {
505 matcher: GlobMatcher,
506 is_absolute: bool,
507}
508
509#[derive(Clone, Debug, Default)]
510pub struct CopilotSettings {
511 /// HTTP/HTTPS proxy to use for Copilot.
512 pub proxy: Option<String>,
513 /// Disable certificate verification for proxy (not recommended).
514 pub proxy_no_verify: Option<bool>,
515 /// Enterprise URI for Copilot.
516 pub enterprise_uri: Option<String>,
517 /// Whether the Copilot Next Edit Suggestions feature is enabled.
518 pub enable_next_edit_suggestions: Option<bool>,
519}
520
521#[derive(Clone, Debug, Default)]
522pub struct CodestralSettings {
523 /// Model to use for completions.
524 pub model: Option<String>,
525 /// Maximum tokens to generate.
526 pub max_tokens: Option<u32>,
527 /// Custom API URL to use for Codestral.
528 pub api_url: Option<String>,
529}
530
531#[derive(Clone, Debug, Default)]
532pub struct OpenAiCompatibleEditPredictionSettings {
533 /// Model to use for completions.
534 pub model: String,
535 /// Maximum tokens to generate.
536 pub max_output_tokens: u32,
537 /// Custom API URL to use for Ollama.
538 pub api_url: Arc<str>,
539 /// The prompt format to use for completions. When `None`, the format
540 /// will be derived from the model name at request time.
541 pub prompt_format: EditPredictionPromptFormat,
542}
543
544#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
545pub enum EditPredictionPromptFormat {
546 #[default]
547 Infer,
548 Zeta(ZetaVersion),
549 CodeLlama,
550 StarCoder,
551 DeepseekCoder,
552 Qwen,
553 CodeGemma,
554 Codestral,
555 Glm,
556}
557
558#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
559pub enum ZetaVersion {
560 Zeta1,
561 Zeta2,
562 #[default] // NOTE: make latest version default when adding
563 Zeta2_1,
564}
565
566impl From<EditPredictionPromptFormatContent> for EditPredictionPromptFormat {
567 fn from(value: EditPredictionPromptFormatContent) -> Self {
568 match value {
569 EditPredictionPromptFormatContent::Infer => Self::Infer,
570 EditPredictionPromptFormatContent::Zeta => Self::Zeta(ZetaVersion::Zeta1),
571 EditPredictionPromptFormatContent::Zeta2 => Self::Zeta(ZetaVersion::Zeta2),
572 EditPredictionPromptFormatContent::Zeta2_1 => Self::Zeta(ZetaVersion::Zeta2_1),
573 EditPredictionPromptFormatContent::CodeLlama => Self::CodeLlama,
574 EditPredictionPromptFormatContent::StarCoder => Self::StarCoder,
575 EditPredictionPromptFormatContent::DeepseekCoder => Self::DeepseekCoder,
576 EditPredictionPromptFormatContent::Qwen => Self::Qwen,
577 EditPredictionPromptFormatContent::CodeGemma => Self::CodeGemma,
578 EditPredictionPromptFormatContent::Codestral => Self::Codestral,
579 EditPredictionPromptFormatContent::Glm => Self::Glm,
580 }
581 }
582}
583
584impl AllLanguageSettings {
585 /// Returns the [`LanguageSettings`] for the language with the specified name.
586 pub fn language<'a>(
587 &'a self,
588 location: Option<SettingsLocation<'a>>,
589 language_name: Option<&LanguageName>,
590 cx: &'a App,
591 ) -> Cow<'a, LanguageSettings> {
592 let settings = language_name
593 .and_then(|name| self.languages.get(name))
594 .unwrap_or(&self.defaults);
595
596 let editorconfig_properties = location.and_then(|location| {
597 cx.global::<SettingsStore>()
598 .editorconfig_store
599 .read(cx)
600 .properties(location.worktree_id, location.path)
601 });
602 if let Some(editorconfig_properties) = editorconfig_properties {
603 let mut settings = settings.clone();
604 merge_with_editorconfig(&mut settings, &editorconfig_properties);
605 Cow::Owned(settings)
606 } else {
607 Cow::Borrowed(settings)
608 }
609 }
610
611 /// Returns whether edit predictions are enabled for the given path.
612 pub fn edit_predictions_enabled_for_file(&self, file: &Arc<dyn File>, cx: &App) -> bool {
613 self.edit_predictions.enabled_for_file(file, cx)
614 }
615
616 /// Returns whether edit predictions are enabled for the given language and path.
617 pub fn show_edit_predictions(&self, language: Option<&Arc<Language>>, cx: &App) -> bool {
618 self.language(None, language.map(|l| l.name()).as_ref(), cx)
619 .show_edit_predictions
620 }
621
622 /// Returns the edit predictions preview mode for the given language and path.
623 pub fn edit_predictions_mode(&self) -> EditPredictionsMode {
624 self.edit_predictions.mode
625 }
626}
627
628fn merge_with_modeline(settings: &mut LanguageSettings, modeline: &ModelineSettings) {
629 let show_whitespaces = modeline.show_trailing_whitespace.and_then(|v| {
630 if v {
631 Some(ShowWhitespaceSetting::Trailing)
632 } else {
633 None
634 }
635 });
636
637 settings
638 .tab_size
639 .merge_from_option(modeline.tab_size.as_ref());
640 settings
641 .hard_tabs
642 .merge_from_option(modeline.hard_tabs.as_ref());
643 settings
644 .preferred_line_length
645 .merge_from_option(modeline.preferred_line_length.map(u32::from).as_ref());
646 let auto_indent_mode = modeline.auto_indent.map(|enabled| {
647 if enabled {
648 AutoIndentMode::SyntaxAware
649 } else {
650 AutoIndentMode::None
651 }
652 });
653 settings
654 .auto_indent
655 .merge_from_option(auto_indent_mode.as_ref());
656 settings
657 .show_whitespaces
658 .merge_from_option(show_whitespaces.as_ref());
659 settings
660 .ensure_final_newline_on_save
661 .merge_from_option(modeline.ensure_final_newline.as_ref());
662}
663
664fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) {
665 let preferred_line_length = cfg.get::<MaxLineLen>().ok().and_then(|v| match v {
666 MaxLineLen::Value(u) => Some(u as u32),
667 MaxLineLen::Off => None,
668 });
669 let tab_size = cfg.get::<IndentSize>().ok().and_then(|v| match v {
670 IndentSize::Value(u) => NonZeroU32::new(u as u32),
671 IndentSize::UseTabWidth => cfg.get::<TabWidth>().ok().and_then(|w| match w {
672 TabWidth::Value(u) => NonZeroU32::new(u as u32),
673 }),
674 });
675 let hard_tabs = cfg
676 .get::<IndentStyle>()
677 .map(|v| v.eq(&IndentStyle::Tabs))
678 .ok();
679 let ensure_final_newline_on_save = cfg
680 .get::<FinalNewline>()
681 .map(|v| match v {
682 FinalNewline::Value(b) => b,
683 })
684 .ok();
685 let remove_trailing_whitespace_on_save = cfg
686 .get::<TrimTrailingWs>()
687 .map(|v| match v {
688 TrimTrailingWs::Value(b) => b,
689 })
690 .ok();
691 let line_ending = cfg.get::<EndOfLine>().ok().and_then(|v| match v {
692 EndOfLine::Lf => Some(LineEndingSetting::EnforceLf),
693 EndOfLine::CrLf => Some(LineEndingSetting::EnforceCrlf),
694 EndOfLine::Cr => None,
695 });
696
697 settings
698 .preferred_line_length
699 .merge_from_option(preferred_line_length.as_ref());
700 settings.tab_size.merge_from_option(tab_size.as_ref());
701 settings.hard_tabs.merge_from_option(hard_tabs.as_ref());
702 // Avoid re-enabling destructive whitespace trimming when Zed settings have
703 // disabled it, e.g. for Markdown hard breaks.
704 if !matches!(remove_trailing_whitespace_on_save, Some(true))
705 || settings.remove_trailing_whitespace_on_save
706 {
707 settings
708 .remove_trailing_whitespace_on_save
709 .merge_from_option(remove_trailing_whitespace_on_save.as_ref());
710 }
711 settings
712 .ensure_final_newline_on_save
713 .merge_from_option(ensure_final_newline_on_save.as_ref());
714 settings.line_ending.merge_from_option(line_ending.as_ref());
715}
716
717impl settings::Settings for AllLanguageSettings {
718 fn from_settings(content: &settings::SettingsContent) -> Self {
719 let all_languages = &content.project.all_languages;
720
721 fn load_from_content(settings: LanguageSettingsContent) -> LanguageSettings {
722 let inlay_hints = settings.inlay_hints.unwrap();
723 let completions = settings.completions.unwrap();
724 let prettier = settings.prettier.unwrap();
725 let indent_guides = settings.indent_guides.unwrap();
726 let tasks = settings.tasks.unwrap();
727 let whitespace_map = settings.whitespace_map.unwrap();
728
729 LanguageSettings {
730 tab_size: settings.tab_size.unwrap(),
731 hard_tabs: settings.hard_tabs.unwrap(),
732 soft_wrap: settings.soft_wrap.unwrap(),
733 preferred_line_length: settings.preferred_line_length.unwrap(),
734 show_wrap_guides: settings.show_wrap_guides.unwrap(),
735 wrap_guides: settings.wrap_guides.unwrap(),
736 indent_guides: IndentGuideSettings {
737 enabled: indent_guides.enabled.unwrap(),
738 line_width: indent_guides.line_width.unwrap(),
739 active_line_width: indent_guides.active_line_width.unwrap(),
740 coloring: indent_guides.coloring.unwrap(),
741 background_coloring: indent_guides.background_coloring.unwrap(),
742 },
743 format_on_save: settings.format_on_save.unwrap(),
744 remove_trailing_whitespace_on_save: settings
745 .remove_trailing_whitespace_on_save
746 .unwrap(),
747 ensure_final_newline_on_save: settings.ensure_final_newline_on_save.unwrap(),
748 line_ending: settings.line_ending.unwrap(),
749 formatter: settings.formatter.unwrap(),
750 prettier: PrettierSettings {
751 allowed: prettier.allowed.unwrap(),
752 parser: prettier.parser.filter(|parser| !parser.is_empty()),
753 plugins: prettier.plugins.unwrap_or_default(),
754 options: prettier.options.unwrap_or_default(),
755 },
756 jsx_tag_auto_close: settings.jsx_tag_auto_close.unwrap().enabled.unwrap(),
757 enable_language_server: settings.enable_language_server.unwrap(),
758 language_servers: settings.language_servers.unwrap(),
759 semantic_tokens: settings.semantic_tokens.unwrap(),
760 document_folding_ranges: settings.document_folding_ranges.unwrap(),
761 document_symbols: settings.document_symbols.unwrap(),
762 allow_rewrap: settings.allow_rewrap.unwrap(),
763 show_edit_predictions: settings.show_edit_predictions.unwrap(),
764 edit_predictions_disabled_in: settings.edit_predictions_disabled_in.unwrap(),
765 show_whitespaces: settings.show_whitespaces.unwrap(),
766 whitespace_map: WhitespaceMap {
767 space: SharedString::new(whitespace_map.space.unwrap().to_string()),
768 tab: SharedString::new(whitespace_map.tab.unwrap().to_string()),
769 },
770 extend_comment_on_newline: settings.extend_comment_on_newline.unwrap(),
771 extend_list_on_newline: settings.extend_list_on_newline.unwrap(),
772 indent_list_on_tab: settings.indent_list_on_tab.unwrap(),
773 inlay_hints: InlayHintSettings {
774 enabled: inlay_hints.enabled.unwrap(),
775 show_value_hints: inlay_hints.show_value_hints.unwrap(),
776 show_type_hints: inlay_hints.show_type_hints.unwrap(),
777 show_parameter_hints: inlay_hints.show_parameter_hints.unwrap(),
778 show_other_hints: inlay_hints.show_other_hints.unwrap(),
779 show_background: inlay_hints.show_background.unwrap(),
780 edit_debounce_ms: inlay_hints.edit_debounce_ms.unwrap(),
781 scroll_debounce_ms: inlay_hints.scroll_debounce_ms.unwrap(),
782 toggle_on_modifiers_press: inlay_hints
783 .toggle_on_modifiers_press
784 .map(|m| m.into_gpui()),
785 },
786 use_autoclose: settings.use_autoclose.unwrap(),
787 use_auto_surround: settings.use_auto_surround.unwrap(),
788 use_on_type_format: settings.use_on_type_format.unwrap(),
789 auto_indent: settings.auto_indent.unwrap(),
790 auto_indent_on_paste: settings.auto_indent_on_paste.unwrap(),
791 always_treat_brackets_as_autoclosed: settings
792 .always_treat_brackets_as_autoclosed
793 .unwrap(),
794 code_actions_on_format: settings.code_actions_on_format.unwrap(),
795 linked_edits: settings.linked_edits.unwrap(),
796 tasks: LanguageTaskSettings {
797 variables: tasks.variables.unwrap_or_default(),
798 enabled: tasks.enabled.unwrap(),
799 prefer_lsp: tasks.prefer_lsp.unwrap(),
800 },
801 show_completions_on_input: settings.show_completions_on_input.unwrap(),
802 show_completion_documentation: settings.show_completion_documentation.unwrap(),
803 colorize_brackets: settings.colorize_brackets.unwrap(),
804 completions: CompletionSettings {
805 words: completions.words.unwrap(),
806 words_min_length: completions.words_min_length.unwrap() as usize,
807 lsp: completions.lsp.unwrap(),
808 lsp_fetch_timeout_ms: completions.lsp_fetch_timeout_ms.unwrap(),
809 lsp_insert_mode: completions.lsp_insert_mode.unwrap(),
810 },
811 debuggers: settings.debuggers.unwrap(),
812 word_diff_enabled: settings.word_diff_enabled.unwrap(),
813 }
814 }
815
816 let default_language_settings = load_from_content(all_languages.defaults.clone());
817
818 let mut languages = HashMap::default();
819 for (language_name, settings) in &all_languages.languages.0 {
820 let mut language_settings = all_languages.defaults.clone();
821 settings::merge_from::MergeFrom::merge_from(&mut language_settings, settings);
822 languages.insert(
823 LanguageName(language_name.clone().into()),
824 load_from_content(language_settings),
825 );
826 }
827
828 let edit_prediction_provider = all_languages
829 .edit_predictions
830 .as_ref()
831 .and_then(|ep| ep.provider);
832
833 let edit_predictions = all_languages.edit_predictions.clone().unwrap();
834 let edit_predictions_mode = edit_predictions.mode.unwrap();
835
836 let disabled_globs: HashSet<&String> = edit_predictions
837 .disabled_globs
838 .as_ref()
839 .unwrap()
840 .iter()
841 .collect();
842
843 let copilot = edit_predictions.copilot.unwrap();
844 let copilot_settings = CopilotSettings {
845 proxy: copilot.proxy,
846 proxy_no_verify: copilot.proxy_no_verify,
847 enterprise_uri: copilot.enterprise_uri,
848 enable_next_edit_suggestions: copilot.enable_next_edit_suggestions,
849 };
850
851 let codestral = edit_predictions.codestral.unwrap();
852 let codestral_settings = CodestralSettings {
853 model: codestral.model,
854 max_tokens: codestral.max_tokens,
855 api_url: codestral.api_url,
856 };
857
858 let ollama = edit_predictions.ollama.unwrap();
859 let ollama_settings = ollama
860 .model
861 .filter(|model| !model.0.is_empty())
862 .map(|model| OpenAiCompatibleEditPredictionSettings {
863 model: model.0,
864 max_output_tokens: ollama.max_output_tokens.unwrap(),
865 api_url: ollama.api_url.unwrap().into(),
866 prompt_format: ollama.prompt_format.unwrap().into(),
867 });
868 let openai_compatible_settings = edit_predictions.open_ai_compatible_api.unwrap();
869 let openai_compatible_settings = openai_compatible_settings
870 .model
871 .filter(|model| !model.is_empty())
872 .zip(
873 openai_compatible_settings
874 .api_url
875 .filter(|api_url| !api_url.is_empty()),
876 )
877 .map(|(model, api_url)| OpenAiCompatibleEditPredictionSettings {
878 model,
879 max_output_tokens: openai_compatible_settings.max_output_tokens.unwrap(),
880 api_url: api_url.into(),
881 prompt_format: openai_compatible_settings.prompt_format.unwrap().into(),
882 });
883
884 let mut file_types: FxHashMap<Arc<str>, (GlobSet, Vec<String>)> = FxHashMap::default();
885
886 for (language, patterns) in all_languages.file_types.iter().flatten() {
887 let mut builder = GlobSetBuilder::new();
888
889 for pattern in &patterns.0 {
890 builder.add(Glob::new(pattern).unwrap());
891 }
892
893 file_types.insert(
894 language.clone(),
895 (
896 builder.build().unwrap(),
897 patterns.0.iter().cloned().collect(),
898 ),
899 );
900 }
901
902 Self {
903 edit_predictions: EditPredictionSettings {
904 provider: if let Some(provider) = edit_prediction_provider {
905 provider
906 } else {
907 EditPredictionProvider::None
908 },
909 disabled_globs: disabled_globs
910 .iter()
911 .filter_map(|g| {
912 let expanded_g = shellexpand::tilde(g).into_owned();
913 Some(DisabledGlob {
914 matcher: globset::Glob::new(&expanded_g).ok()?.compile_matcher(),
915 is_absolute: Path::new(&expanded_g).is_absolute(),
916 })
917 })
918 .collect(),
919 mode: edit_predictions_mode,
920 copilot: copilot_settings,
921 codestral: codestral_settings,
922 ollama: ollama_settings,
923 open_ai_compatible_api: openai_compatible_settings,
924 allow_data_collection: edit_predictions.allow_data_collection.unwrap_or_default(),
925 },
926 defaults: default_language_settings,
927 languages,
928 file_types,
929 }
930 }
931}
932
933#[derive(Default, Debug, Clone, PartialEq, Eq)]
934pub struct JsxTagAutoCloseSettings {
935 /// Enables or disables auto-closing of JSX tags.
936 pub enabled: bool,
937}
938
939#[cfg(test)]
940mod tests {
941 use super::*;
942 use gpui::TestAppContext;
943 use settings::{LocalSettingsKind, LocalSettingsPath, WorktreeId};
944 use util::rel_path::rel_path;
945
946 #[gpui::test]
947 fn test_edit_predictions_enabled_for_file(cx: &mut TestAppContext) {
948 use crate::TestFile;
949 use std::path::PathBuf;
950
951 let cx = cx.app.borrow_mut();
952
953 let build_settings = |globs: &[&str]| -> EditPredictionSettings {
954 EditPredictionSettings {
955 disabled_globs: globs
956 .iter()
957 .map(|glob_str| {
958 #[cfg(windows)]
959 let glob_str = {
960 let mut g = String::new();
961
962 if glob_str.starts_with('/') {
963 g.push_str("C:");
964 }
965
966 g.push_str(&glob_str.replace('/', "\\"));
967 g
968 };
969 #[cfg(windows)]
970 let glob_str = glob_str.as_str();
971 let expanded_glob_str = shellexpand::tilde(glob_str).into_owned();
972 DisabledGlob {
973 matcher: globset::Glob::new(&expanded_glob_str)
974 .unwrap()
975 .compile_matcher(),
976 is_absolute: Path::new(&expanded_glob_str).is_absolute(),
977 }
978 })
979 .collect(),
980 ..Default::default()
981 }
982 };
983
984 const WORKTREE_NAME: &str = "project";
985 let make_test_file = |segments: &[&str]| -> Arc<dyn File> {
986 let path = segments.join("/");
987 let path = rel_path(&path);
988
989 Arc::new(TestFile {
990 path: path.into(),
991 root_name: WORKTREE_NAME.to_string(),
992 local_root: Some(PathBuf::from(if cfg!(windows) {
993 "C:\\absolute\\"
994 } else {
995 "/absolute/"
996 })),
997 })
998 };
999
1000 let test_file = make_test_file(&["src", "test", "file.rs"]);
1001
1002 // Test relative globs
1003 let settings = build_settings(&["*.rs"]);
1004 assert!(!settings.enabled_for_file(&test_file, &cx));
1005 let settings = build_settings(&["*.txt"]);
1006 assert!(settings.enabled_for_file(&test_file, &cx));
1007
1008 // Test absolute globs
1009 let settings = build_settings(&["/absolute/**/*.rs"]);
1010 assert!(!settings.enabled_for_file(&test_file, &cx));
1011 let settings = build_settings(&["/other/**/*.rs"]);
1012 assert!(settings.enabled_for_file(&test_file, &cx));
1013
1014 // Test exact path match relative
1015 let settings = build_settings(&["src/test/file.rs"]);
1016 assert!(!settings.enabled_for_file(&test_file, &cx));
1017 let settings = build_settings(&["src/test/otherfile.rs"]);
1018 assert!(settings.enabled_for_file(&test_file, &cx));
1019
1020 // Test exact path match absolute
1021 let settings = build_settings(&[&format!("/absolute/{}/src/test/file.rs", WORKTREE_NAME)]);
1022 assert!(!settings.enabled_for_file(&test_file, &cx));
1023 let settings = build_settings(&["/other/test/otherfile.rs"]);
1024 assert!(settings.enabled_for_file(&test_file, &cx));
1025
1026 // Test * glob
1027 let settings = build_settings(&["*"]);
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 **/* glob
1033 let settings = build_settings(&["**/*"]);
1034 assert!(!settings.enabled_for_file(&test_file, &cx));
1035 let settings = build_settings(&["other/**/*"]);
1036 assert!(settings.enabled_for_file(&test_file, &cx));
1037
1038 // Test directory/** glob
1039 let settings = build_settings(&["src/**"]);
1040 assert!(!settings.enabled_for_file(&test_file, &cx));
1041
1042 let test_file_root: Arc<dyn File> = Arc::new(TestFile {
1043 path: rel_path("file.rs").into(),
1044 root_name: WORKTREE_NAME.to_string(),
1045 local_root: Some(PathBuf::from("/absolute/")),
1046 });
1047 assert!(settings.enabled_for_file(&test_file_root, &cx));
1048
1049 let settings = build_settings(&["other/**"]);
1050 assert!(settings.enabled_for_file(&test_file, &cx));
1051
1052 // Test **/directory/* glob
1053 let settings = build_settings(&["**/test/*"]);
1054 assert!(!settings.enabled_for_file(&test_file, &cx));
1055 let settings = build_settings(&["**/other/*"]);
1056 assert!(settings.enabled_for_file(&test_file, &cx));
1057
1058 // Test multiple globs
1059 let settings = build_settings(&["*.rs", "*.txt", "src/**"]);
1060 assert!(!settings.enabled_for_file(&test_file, &cx));
1061 let settings = build_settings(&["*.txt", "*.md", "other/**"]);
1062 assert!(settings.enabled_for_file(&test_file, &cx));
1063
1064 // Test dot files
1065 let dot_file = make_test_file(&[".config", "settings.json"]);
1066 let settings = build_settings(&[".*/**"]);
1067 assert!(!settings.enabled_for_file(&dot_file, &cx));
1068
1069 let dot_env_file = make_test_file(&[".env"]);
1070 let settings = build_settings(&[".env"]);
1071 assert!(!settings.enabled_for_file(&dot_env_file, &cx));
1072
1073 // Test tilde expansion
1074 let home = shellexpand::tilde("~").into_owned();
1075 let home_file = Arc::new(TestFile {
1076 path: rel_path("test.rs").into(),
1077 root_name: "the-dir".to_string(),
1078 local_root: Some(PathBuf::from(home)),
1079 }) as Arc<dyn File>;
1080 let settings = build_settings(&["~/the-dir/test.rs"]);
1081 assert!(!settings.enabled_for_file(&home_file, &cx));
1082 }
1083
1084 #[test]
1085 fn test_resolve_language_servers() {
1086 fn language_server_names(names: &[&str]) -> Vec<LanguageServerName> {
1087 names
1088 .iter()
1089 .copied()
1090 .map(|name| LanguageServerName(name.to_string().into()))
1091 .collect::<Vec<_>>()
1092 }
1093
1094 let available_language_servers = language_server_names(&[
1095 "typescript-language-server",
1096 "biome",
1097 "deno",
1098 "eslint",
1099 "tailwind",
1100 ]);
1101
1102 // A value of just `["..."]` is the same as taking all of the available language servers.
1103 assert_eq!(
1104 LanguageSettings::resolve_language_servers(
1105 &[REST_OF_LANGUAGE_SERVERS.into()],
1106 &available_language_servers,
1107 ),
1108 available_language_servers
1109 );
1110
1111 // Referencing one of the available language servers will change its order.
1112 assert_eq!(
1113 LanguageSettings::resolve_language_servers(
1114 &[
1115 "biome".into(),
1116 REST_OF_LANGUAGE_SERVERS.into(),
1117 "deno".into()
1118 ],
1119 &available_language_servers
1120 ),
1121 language_server_names(&[
1122 "biome",
1123 "typescript-language-server",
1124 "eslint",
1125 "tailwind",
1126 "deno",
1127 ])
1128 );
1129
1130 // Negating an available language server removes it from the list.
1131 assert_eq!(
1132 LanguageSettings::resolve_language_servers(
1133 &[
1134 "deno".into(),
1135 "!typescript-language-server".into(),
1136 "!biome".into(),
1137 REST_OF_LANGUAGE_SERVERS.into()
1138 ],
1139 &available_language_servers
1140 ),
1141 language_server_names(&["deno", "eslint", "tailwind"])
1142 );
1143
1144 // Adding a language server not in the list of available language servers adds it to the list.
1145 assert_eq!(
1146 LanguageSettings::resolve_language_servers(
1147 &[
1148 "my-cool-language-server".into(),
1149 REST_OF_LANGUAGE_SERVERS.into()
1150 ],
1151 &available_language_servers
1152 ),
1153 language_server_names(&[
1154 "my-cool-language-server",
1155 "typescript-language-server",
1156 "biome",
1157 "deno",
1158 "eslint",
1159 "tailwind",
1160 ])
1161 );
1162 }
1163
1164 #[gpui::test]
1165 fn test_language_servers_across_settings_files(cx: &mut TestAppContext) {
1166 cx.update(|cx| {
1167 let mut store = SettingsStore::new(cx, &settings::default_settings());
1168 store.register_setting::<AllLanguageSettings>();
1169
1170 let worktree_id = WorktreeId::from_usize(1);
1171 let root = LocalSettingsPath::InWorktree(rel_path("root").into());
1172 let subdir = LocalSettingsPath::InWorktree(rel_path("root/subdir").into());
1173 let root_location = Some(SettingsLocation {
1174 worktree_id,
1175 path: rel_path("root/a.ts"),
1176 });
1177 let subdir_location = Some(SettingsLocation {
1178 worktree_id,
1179 path: rel_path("root/subdir/b.ts"),
1180 });
1181
1182 assert_eq!(
1183 resolved_language_servers(&store, None, "TypeScript"),
1184 servers(&["vtsls", "eslint"]),
1185 "default settings should disable typescript-language-server for TypeScript"
1186 );
1187 assert_eq!(
1188 resolved_language_servers(&store, None, "Rust"),
1189 servers(&["typescript-language-server", "vtsls", "eslint"]),
1190 );
1191
1192 store
1193 .set_user_settings(r#"{"language_servers": ["!vtsls", "..."]}"#, cx)
1194 .unwrap();
1195 assert_eq!(
1196 resolved_language_servers(&store, None, "TypeScript"),
1197 servers(&["vtsls", "eslint"]),
1198 "the per-language list should fully replace the user's global list"
1199 );
1200 assert_eq!(
1201 resolved_language_servers(&store, None, "Rust"),
1202 servers(&["typescript-language-server", "eslint"]),
1203 "user's global disable should apply to languages without their own list"
1204 );
1205
1206 store
1207 .set_local_settings(
1208 worktree_id,
1209 root.clone(),
1210 LocalSettingsKind::Settings,
1211 Some(r#"{"language_servers": ["..."]}"#),
1212 cx,
1213 )
1214 .unwrap();
1215 assert_eq!(
1216 resolved_language_servers(&store, root_location, "Rust"),
1217 servers(&["typescript-language-server", "vtsls", "eslint"]),
1218 "project settings enabling all servers should undo the user's global disable (#61524)"
1219 );
1220 assert_eq!(
1221 resolved_language_servers(&store, None, "Rust"),
1222 servers(&["typescript-language-server", "eslint"]),
1223 "user's global disable should still apply outside of the project"
1224 );
1225
1226 store
1227 .set_local_settings(
1228 worktree_id,
1229 root.clone(),
1230 LocalSettingsKind::Settings,
1231 Some(r#"{"language_servers": ["!eslint", "..."]}"#),
1232 cx,
1233 )
1234 .unwrap();
1235 assert_eq!(
1236 resolved_language_servers(&store, root_location, "Rust"),
1237 servers(&["typescript-language-server", "vtsls"]),
1238 "project's global list should replace the user's global list"
1239 );
1240 assert_eq!(
1241 resolved_language_servers(&store, root_location, "TypeScript"),
1242 servers(&["vtsls", "eslint"]),
1243 "the per-language list should fully replace the project's global list"
1244 );
1245
1246 store
1247 .set_local_settings(
1248 worktree_id,
1249 subdir.clone(),
1250 LocalSettingsKind::Settings,
1251 Some(r#"{"language_servers": ["..."]}"#),
1252 cx,
1253 )
1254 .unwrap();
1255 assert_eq!(
1256 resolved_language_servers(&store, subdir_location, "Rust"),
1257 servers(&["typescript-language-server", "vtsls", "eslint"]),
1258 "nested project settings should replace the outer global list"
1259 );
1260 assert_eq!(
1261 resolved_language_servers(&store, root_location, "Rust"),
1262 servers(&["typescript-language-server", "vtsls"]),
1263 );
1264 store
1265 .set_local_settings(worktree_id, subdir, LocalSettingsKind::Settings, None, cx)
1266 .unwrap();
1267
1268 store
1269 .set_local_settings(
1270 worktree_id,
1271 root.clone(),
1272 LocalSettingsKind::Settings,
1273 Some(
1274 r#"{"languages": {"TypeScript": {"language_servers": ["vtsls", "..."]}}}"#,
1275 ),
1276 cx,
1277 )
1278 .unwrap();
1279 assert_eq!(
1280 resolved_language_servers(&store, root_location, "TypeScript"),
1281 servers(&["vtsls", "typescript-language-server", "eslint"]),
1282 "project's per-language configuration should override the user's global disable"
1283 );
1284 assert_eq!(
1285 resolved_language_servers(&store, root_location, "Rust"),
1286 servers(&["typescript-language-server", "eslint"]),
1287 "user's global disable should still apply to other languages"
1288 );
1289
1290 store
1291 .set_user_settings(
1292 r#"{
1293 "language_servers": ["!vtsls", "..."],
1294 "languages": {"TypeScript": {"language_servers": ["vtsls", "..."]}}
1295 }"#,
1296 cx,
1297 )
1298 .unwrap();
1299 assert_eq!(
1300 resolved_language_servers(&store, None, "TypeScript"),
1301 servers(&["vtsls", "typescript-language-server", "eslint"]),
1302 "per-language configuration should win over a global disable from the same file"
1303 );
1304 assert_eq!(
1305 resolved_language_servers(&store, None, "Rust"),
1306 servers(&["typescript-language-server", "eslint"]),
1307 );
1308
1309 store
1310 .set_local_settings(
1311 worktree_id,
1312 root.clone(),
1313 LocalSettingsKind::Settings,
1314 Some(r#"{"language_servers": ["!vtsls", "..."]}"#),
1315 cx,
1316 )
1317 .unwrap();
1318 assert_eq!(
1319 resolved_language_servers(&store, root_location, "TypeScript"),
1320 servers(&["vtsls", "typescript-language-server", "eslint"]),
1321 "user's per-language list should win over the project's global disable"
1322 );
1323 assert_eq!(
1324 resolved_language_servers(&store, root_location, "Rust"),
1325 servers(&["typescript-language-server", "eslint"]),
1326 "project's global disable should still apply to languages without their own list"
1327 );
1328
1329 store
1330 .set_user_settings(
1331 r#"{"languages": {"JavaScript": {"language_servers": ["!vtsls", "..."]}}}"#,
1332 cx,
1333 )
1334 .unwrap();
1335 store
1336 .set_local_settings(
1337 worktree_id,
1338 root,
1339 LocalSettingsKind::Settings,
1340 Some(r#"{"languages": {"JavaScript": {"language_servers": ["..."]}}}"#),
1341 cx,
1342 )
1343 .unwrap();
1344 assert_eq!(
1345 resolved_language_servers(&store, root_location, "JavaScript"),
1346 servers(&["typescript-language-server", "vtsls", "eslint"]),
1347 "project's per-language list should re-enable a server disabled by the user (#61524)"
1348 );
1349 assert_eq!(
1350 resolved_language_servers(&store, None, "JavaScript"),
1351 servers(&["typescript-language-server", "eslint"]),
1352 "user's per-language disable should still apply outside of the project"
1353 );
1354 });
1355 }
1356
1357 #[gpui::test]
1358 fn test_language_servers_combined_restrictions(cx: &mut TestAppContext) {
1359 cx.update(|cx| {
1360 let mut store = SettingsStore::new(cx, &settings::default_settings());
1361 store.register_setting::<AllLanguageSettings>();
1362
1363 let worktree_id = WorktreeId::from_usize(1);
1364 let root = LocalSettingsPath::InWorktree(rel_path("root").into());
1365 let root_location = Some(SettingsLocation {
1366 worktree_id,
1367 path: rel_path("root/a.ts"),
1368 });
1369
1370 store
1371 .set_user_settings(r#"{"language_servers": ["!vtsls", "..."]}"#, cx)
1372 .unwrap();
1373 store
1374 .set_local_settings(
1375 worktree_id,
1376 root,
1377 LocalSettingsKind::Settings,
1378 Some(r#"{"language_servers": ["!eslint", "..."]}"#),
1379 cx,
1380 )
1381 .unwrap();
1382 assert_eq!(
1383 resolved_language_servers(&store, root_location, "Rust"),
1384 servers(&["typescript-language-server", "vtsls"]),
1385 "global lists replace wholesale: disables from different files must not accumulate"
1386 );
1387 assert_eq!(
1388 resolved_language_servers(&store, None, "Rust"),
1389 servers(&["typescript-language-server", "eslint"]),
1390 "user's own disable should still apply outside of the project"
1391 );
1392 assert_eq!(
1393 resolved_language_servers(&store, root_location, "TypeScript"),
1394 servers(&["vtsls", "eslint"]),
1395 "a language with its own list should ignore global lists from every file"
1396 );
1397
1398 store
1399 .set_user_settings(r#"{"language_servers": ["...", "!vtsls"]}"#, cx)
1400 .unwrap();
1401 assert_eq!(
1402 resolved_language_servers(&store, None, "Rust"),
1403 servers(&["typescript-language-server", "eslint"]),
1404 "the position of a disabled entry relative to '...' should not matter"
1405 );
1406
1407 store
1408 .set_user_settings(r#"{"language_servers": ["eslint", "..."]}"#, cx)
1409 .unwrap();
1410 assert_eq!(
1411 resolved_language_servers(&store, None, "Rust"),
1412 servers(&["eslint", "typescript-language-server", "vtsls"]),
1413 "the position of '...' should determine the priority order of enabled servers"
1414 );
1415
1416 store
1417 .set_user_settings(r#"{"language_servers": ["eslint"]}"#, cx)
1418 .unwrap();
1419 assert_eq!(
1420 resolved_language_servers(&store, None, "Rust"),
1421 servers(&["eslint"]),
1422 "a global list without '...' should be exhaustive for languages without their own list"
1423 );
1424 assert_eq!(
1425 resolved_language_servers(&store, None, "TypeScript"),
1426 servers(&["vtsls", "eslint"]),
1427 "a global list, exhaustive or not, never applies to languages with their own list"
1428 );
1429
1430 store
1431 .set_user_settings(
1432 r#"{
1433 "language_servers": ["!eslint", "..."],
1434 "languages": {"TypeScript": {"language_servers": ["vtsls", "..."]}}
1435 }"#,
1436 cx,
1437 )
1438 .unwrap();
1439 assert_eq!(
1440 resolved_language_servers(&store, None, "TypeScript"),
1441 servers(&["vtsls", "typescript-language-server", "eslint"]),
1442 "authoring a per-language list opts the language out of the global list entirely"
1443 );
1444 assert_eq!(
1445 resolved_language_servers(&store, None, "Rust"),
1446 servers(&["typescript-language-server", "vtsls"]),
1447 );
1448
1449 // Issue #60763: to disable a server that the shipped list explicitly
1450 // enables, the spelling is per-language, restating the shipped
1451 // exclusions.
1452 store
1453 .set_user_settings(
1454 r#"{"languages": {"TypeScript": {"language_servers": ["!typescript-language-server", "!vtsls", "..."]}}}"#,
1455 cx,
1456 )
1457 .unwrap();
1458 assert_eq!(
1459 resolved_language_servers(&store, None, "TypeScript"),
1460 servers(&["eslint"]),
1461 "a per-language disable should win over the shipped explicit enable"
1462 );
1463 store
1464 .set_user_settings(
1465 r#"{"languages": {"TypeScript": {"language_servers": ["!vtsls", "..."]}}}"#,
1466 cx,
1467 )
1468 .unwrap();
1469 assert_eq!(
1470 resolved_language_servers(&store, None, "TypeScript"),
1471 servers(&["typescript-language-server", "eslint"]),
1472 "a per-language list not restating the shipped exclusions should lift them"
1473 );
1474
1475 store
1476 .set_user_settings(
1477 r#"{"languages": {"TypeScript": {"language_servers": ["eslint"]}}}"#,
1478 cx,
1479 )
1480 .unwrap();
1481 assert_eq!(
1482 resolved_language_servers(&store, None, "TypeScript"),
1483 servers(&["eslint"]),
1484 "an exhaustive per-language list should pin exactly the listed servers"
1485 );
1486 });
1487 }
1488
1489 fn servers(names: &[&str]) -> Vec<LanguageServerName> {
1490 names
1491 .iter()
1492 .map(|name| LanguageServerName(name.to_string().into()))
1493 .collect::<Vec<_>>()
1494 }
1495
1496 fn resolved_language_servers(
1497 store: &SettingsStore,
1498 location: Option<SettingsLocation>,
1499 language: &str,
1500 ) -> Vec<LanguageServerName> {
1501 let all_settings = store.get::<AllLanguageSettings>(location);
1502 let language_settings = all_settings
1503 .languages
1504 .get(&LanguageName::new(language))
1505 .unwrap_or(&all_settings.defaults);
1506 language_settings.customized_language_servers(&servers(&[
1507 "typescript-language-server",
1508 "vtsls",
1509 "eslint",
1510 ]))
1511 }
1512}
1513