1use crate::*;
2use anyhow::{Context as _, Result, anyhow};
3use collections::HashMap;
4use fs::Fs;
5use gpui::Rgba;
6use paths::{cursor_settings_file_paths, vscode_settings_file_paths};
7use serde::Deserialize;
8use serde_json::{Map, Value};
9use std::{
10 num::{NonZeroU32, NonZeroUsize},
11 path::{Path, PathBuf},
12 sync::Arc,
13};
14
15#[derive(Clone, Copy, PartialEq, Eq, Debug)]
16pub enum VsCodeSettingsSource {
17 VsCode,
18 Cursor,
19}
20
21impl std::fmt::Display for VsCodeSettingsSource {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 match self {
24 VsCodeSettingsSource::VsCode => write!(f, "VS Code"),
25 VsCodeSettingsSource::Cursor => write!(f, "Cursor"),
26 }
27 }
28}
29
30pub struct VsCodeSettings {
31 pub source: VsCodeSettingsSource,
32 pub path: Arc<Path>,
33 content: Map<String, Value>,
34}
35
36impl VsCodeSettings {
37 #[cfg(any(test, feature = "test-support"))]
38 pub fn from_str(content: &str, source: VsCodeSettingsSource) -> Result<Self> {
39 Ok(Self {
40 source,
41 path: Path::new("/example-path/Code/User/settings.json").into(),
42 content: serde_json_lenient::from_str(content)?,
43 })
44 }
45
46 pub async fn load_user_settings(source: VsCodeSettingsSource, fs: Arc<dyn Fs>) -> Result<Self> {
47 let candidate_paths = match source {
48 VsCodeSettingsSource::VsCode => vscode_settings_file_paths(),
49 VsCodeSettingsSource::Cursor => cursor_settings_file_paths(),
50 };
51 let mut path = None;
52 for candidate_path in candidate_paths.iter() {
53 if fs.is_file(candidate_path).await {
54 path = Some(candidate_path.clone());
55 }
56 }
57 let Some(path) = path else {
58 return Err(anyhow!(
59 "No settings file found, expected to find it in one of the following paths:\n{}",
60 candidate_paths
61 .into_iter()
62 .map(|path| path.to_string_lossy().into_owned())
63 .collect::<Vec<_>>()
64 .join("\n")
65 ));
66 };
67 let content = fs.load(&path).await.with_context(|| {
68 format!(
69 "Error loading {} settings file from {}",
70 source,
71 path.display()
72 )
73 })?;
74 let content = serde_json_lenient::from_str(&content).with_context(|| {
75 format!(
76 "Error parsing {} settings file from {}",
77 source,
78 path.display()
79 )
80 })?;
81 Ok(Self {
82 source,
83 path: path.into(),
84 content,
85 })
86 }
87
88 fn read_value(&self, setting: &str) -> Option<&Value> {
89 self.content.get(setting)
90 }
91
92 fn read_str(&self, setting: &str) -> Option<&str> {
93 self.read_value(setting).and_then(|v| v.as_str())
94 }
95
96 fn read_string(&self, setting: &str) -> Option<String> {
97 self.read_value(setting)
98 .and_then(|v| v.as_str())
99 .map(|s| s.to_owned())
100 }
101
102 fn read_bool(&self, setting: &str) -> Option<bool> {
103 self.read_value(setting).and_then(|v| v.as_bool())
104 }
105
106 fn read_f32(&self, setting: &str) -> Option<f32> {
107 self.read_value(setting)
108 .and_then(|v| v.as_f64())
109 .map(|v| v as f32)
110 }
111
112 fn read_u64(&self, setting: &str) -> Option<u64> {
113 self.read_value(setting).and_then(|v| v.as_u64())
114 }
115
116 fn read_usize(&self, setting: &str) -> Option<usize> {
117 self.read_value(setting)
118 .and_then(|v| v.as_u64())
119 .and_then(|v| v.try_into().ok())
120 }
121
122 fn read_u32(&self, setting: &str) -> Option<u32> {
123 self.read_value(setting)
124 .and_then(|v| v.as_u64())
125 .and_then(|v| v.try_into().ok())
126 }
127
128 fn read_enum<T>(&self, key: &str, f: impl FnOnce(&str) -> Option<T>) -> Option<T> {
129 self.content.get(key).and_then(Value::as_str).and_then(f)
130 }
131
132 fn read_fonts(&self, key: &str) -> (Option<FontFamilyName>, Option<Vec<FontFamilyName>>) {
133 let Some(css_name) = self.content.get(key).and_then(Value::as_str) else {
134 return (None, None);
135 };
136
137 let mut name_buffer = String::new();
138 let mut quote_char: Option<char> = None;
139 let mut fonts = Vec::new();
140 let mut add_font = |buffer: &mut String| {
141 let trimmed = buffer.trim();
142 if !trimmed.is_empty() {
143 fonts.push(trimmed.to_string().into());
144 }
145
146 buffer.clear();
147 };
148
149 for ch in css_name.chars() {
150 match (ch, quote_char) {
151 ('"' | '\'', None) => {
152 quote_char = Some(ch);
153 }
154 (_, Some(q)) if ch == q => {
155 quote_char = None;
156 }
157 (',', None) => {
158 add_font(&mut name_buffer);
159 }
160 _ => {
161 name_buffer.push(ch);
162 }
163 }
164 }
165
166 add_font(&mut name_buffer);
167 if fonts.is_empty() {
168 return (None, None);
169 }
170 (Some(fonts.remove(0)), skip_default(fonts))
171 }
172
173 pub fn settings_content(&self) -> SettingsContent {
174 SettingsContent {
175 agent: self.agent_settings_content(),
176 agent_servers: None,
177 audio: None,
178 auto_update: None,
179 base_keymap: Some(BaseKeymapContent::VSCode),
180 calls: None,
181 collaboration_panel: None,
182 credentials_url: None,
183 debugger: None,
184 diagnostics: None,
185 editor: self.editor_settings_content(),
186 extension: ExtensionSettingsContent::default(),
187 file_finder: None,
188 git: self.git_settings_content(),
189 git_panel: self.git_panel_settings_content(),
190 global_lsp_settings: skip_default(GlobalLspSettingsContent {
191 semantic_token_rules: self.semantic_token_rules(),
192 ..GlobalLspSettingsContent::default()
193 }),
194 helix_mode: None,
195 image_viewer: None,
196 journal: None,
197 language_models: None,
198 line_indicator_format: None,
199 log: None,
200 message_editor: None,
201 node: self.node_binary_settings(),
202
203 outline_panel: self.outline_panel_settings_content(),
204 preview_tabs: self.preview_tabs_settings_content(),
205 project: self.project_settings_content(),
206 project_panel: self.project_panel_settings_content(),
207 proxy: self.read_string("http.proxy"),
208 remote: RemoteSettingsContent::default(),
209 repl: None,
210 server_url: None,
211 session: None,
212 status_bar: self.status_bar_settings_content(),
213 tab_bar: self.tab_bar_settings_content(),
214 tabs: self.item_settings_content(),
215 telemetry: self.telemetry_settings_content(),
216 terminal: self.terminal_settings_content(),
217 theme: Box::new(self.theme_settings_content()),
218 title_bar: None,
219 vim: None,
220 vim_mode: None,
221 workspace: self.workspace_settings_content(),
222 which_key: None,
223 modeline_lines: None,
224 feature_flags: None,
225 }
226 }
227
228 fn agent_settings_content(&self) -> Option<AgentSettingsContent> {
229 let enabled = self.read_bool("chat.agent.enabled");
230 skip_default(AgentSettingsContent {
231 enabled: enabled,
232 button: enabled,
233 ..Default::default()
234 })
235 }
236
237 fn editor_settings_content(&self) -> EditorSettingsContent {
238 EditorSettingsContent {
239 auto_signature_help: self.read_bool("editor.parameterHints.enabled"),
240 autoscroll_on_clicks: None,
241 cursor_blink: self.read_enum("editor.cursorBlinking", |s| match s {
242 "blink" | "phase" | "expand" | "smooth" => Some(true),
243 "solid" => Some(false),
244 _ => None,
245 }),
246 cursor_shape: self.read_enum("editor.cursorStyle", |s| match s {
247 "block" => Some(CursorShape::Block),
248 "block-outline" => Some(CursorShape::Hollow),
249 "line" | "line-thin" => Some(CursorShape::Bar),
250 "underline" | "underline-thin" => Some(CursorShape::Underline),
251 _ => None,
252 }),
253 current_line_highlight: self.read_enum("editor.renderLineHighlight", |s| match s {
254 "gutter" => Some(CurrentLineHighlight::Gutter),
255 "line" => Some(CurrentLineHighlight::Line),
256 "all" => Some(CurrentLineHighlight::All),
257 _ => None,
258 }),
259 diagnostics_max_severity: None,
260 double_click_in_multibuffer: None,
261 drag_and_drop_selection: None,
262 excerpt_context_lines: None,
263 expand_excerpt_lines: None,
264 fast_scroll_sensitivity: self.read_f32("editor.fastScrollSensitivity"),
265 sticky_scroll: self.sticky_scroll_content(),
266 go_to_definition_fallback: None,
267 gutter: self.gutter_content(),
268 hide_mouse: None,
269 horizontal_scroll_margin: None,
270 hover_popover_delay: self.read_u64("editor.hover.delay").map(Into::into),
271 hover_popover_enabled: self.read_bool("editor.hover.enabled"),
272 hover_popover_sticky: self.read_bool("editor.hover.sticky"),
273 hover_popover_hiding_delay: self.read_u64("editor.hover.hidingDelay").map(Into::into),
274 inline_code_actions: None,
275 code_lens: None,
276 jupyter: None,
277 lsp_document_colors: None,
278 lsp_highlight_debounce: None,
279 middle_click_paste: None,
280 minimap: self.minimap_content(),
281 minimum_contrast_for_highlights: None,
282 multi_cursor_modifier: self.read_enum("editor.multiCursorModifier", |s| match s {
283 "ctrlCmd" => Some(MultiCursorModifier::CmdOrCtrl),
284 "alt" => Some(MultiCursorModifier::Alt),
285 _ => None,
286 }),
287 redact_private_values: None,
288 relative_line_numbers: self.read_enum("editor.lineNumbers", |s| match s {
289 "relative" => Some(RelativeLineNumbers::Enabled),
290 _ => None,
291 }),
292 rounded_selection: self.read_bool("editor.roundedSelection"),
293 scroll_beyond_last_line: None,
294 mouse_wheel_zoom: self.read_bool("editor.mouseWheelZoom"),
295 scroll_sensitivity: self.read_f32("editor.mouseWheelScrollSensitivity"),
296 scrollbar: self.scrollbar_content(),
297 search: self.search_content(),
298 search_wrap: None,
299 seed_search_query_from_cursor: self.read_enum(
300 "editor.find.seedSearchStringFromSelection",
301 |s| match s {
302 "always" => Some(SeedQuerySetting::Always),
303 "selection" => Some(SeedQuerySetting::Selection),
304 "never" => Some(SeedQuerySetting::Never),
305 _ => None,
306 },
307 ),
308 selection_highlight: self.read_bool("editor.selectionHighlight"),
309 show_signature_help_after_edits: self.read_bool("editor.parameterHints.enabled"),
310 snippet_sort_order: None,
311 toolbar: None,
312 use_smartcase_search: self.read_bool("search.smartCase"),
313 vertical_scroll_margin: self.read_f32("editor.cursorSurroundingLines"),
314 completion_menu_scrollbar: None,
315 completion_detail_alignment: None,
316 diff_view_style: None,
317 minimum_split_diff_width: None,
318 }
319 }
320
321 fn sticky_scroll_content(&self) -> Option<StickyScrollContent> {
322 skip_default(StickyScrollContent {
323 enabled: self.read_bool("editor.stickyScroll.enabled"),
324 })
325 }
326
327 fn gutter_content(&self) -> Option<GutterContent> {
328 skip_default(GutterContent {
329 line_numbers: self.read_enum("editor.lineNumbers", |s| match s {
330 "on" | "relative" => Some(true),
331 "off" => Some(false),
332 _ => None,
333 }),
334 min_line_number_digits: None,
335 runnables: None,
336 breakpoints: None,
337 bookmarks: None,
338 folds: self.read_enum("editor.showFoldingControls", |s| match s {
339 "always" | "mouseover" => Some(true),
340 "never" => Some(false),
341 _ => None,
342 }),
343 })
344 }
345
346 fn scrollbar_content(&self) -> Option<ScrollbarContent> {
347 let scrollbar_axes = skip_default(ScrollbarAxesContent {
348 horizontal: self.read_enum("editor.scrollbar.horizontal", |s| match s {
349 "auto" | "visible" => Some(true),
350 "hidden" => Some(false),
351 _ => None,
352 }),
353 vertical: self.read_enum("editor.scrollbar.vertical", |s| match s {
354 "auto" | "visible" => Some(true),
355 "hidden" => Some(false),
356 _ => None,
357 }),
358 })?;
359
360 Some(ScrollbarContent {
361 axes: Some(scrollbar_axes),
362 ..Default::default()
363 })
364 }
365
366 fn search_content(&self) -> Option<SearchSettingsContent> {
367 skip_default(SearchSettingsContent {
368 include_ignored: self.read_bool("search.useIgnoreFiles"),
369 ..Default::default()
370 })
371 }
372
373 fn semantic_token_rules(&self) -> Option<SemanticTokenRules> {
374 let customizations = self
375 .read_value("editor.semanticTokenColorCustomizations")?
376 .as_object()?;
377
378 skip_default(SemanticTokenRules {
379 rules: customizations
380 .get("rules")
381 .and_then(|v| {
382 Some(
383 v.as_object()?
384 .iter()
385 .filter_map(|(k, v)| {
386 let v = v.as_object()?;
387
388 let mut underline = v
389 .get("underline")
390 .and_then(|b| b.as_bool())
391 .unwrap_or(false);
392 let strikethrough = v
393 .get("strikethrough")
394 .and_then(|b| b.as_bool())
395 .unwrap_or(false);
396 let mut font_weight =
397 v.get("bold").and_then(|b| b.as_bool()).map(|b| {
398 if b {
399 SemanticTokenFontWeight::Bold
400 } else {
401 SemanticTokenFontWeight::Normal
402 }
403 });
404 let mut font_style =
405 v.get("italic").and_then(|b| b.as_bool()).map(|b| {
406 if b {
407 SemanticTokenFontStyle::Italic
408 } else {
409 SemanticTokenFontStyle::Normal
410 }
411 });
412
413 match v.get("fontStyle").and_then(|s| s.as_str()).unwrap_or("") {
414 "bold" => {
415 font_style = Some(SemanticTokenFontStyle::Normal);
416 font_weight = Some(SemanticTokenFontWeight::Bold);
417 }
418 "italic" => {
419 font_style = Some(SemanticTokenFontStyle::Italic);
420 font_weight = Some(SemanticTokenFontWeight::Normal);
421 }
422 "underline" => {
423 underline = true;
424 }
425 "bold italic" | "italic bold" => {
426 font_style = Some(SemanticTokenFontStyle::Italic);
427 font_weight = Some(SemanticTokenFontWeight::Bold);
428 }
429 "normal" => {
430 font_style = Some(SemanticTokenFontStyle::Normal);
431 font_weight = Some(SemanticTokenFontWeight::Normal);
432 }
433 _ => {}
434 }
435
436 let foreground = v
437 .get("foreground")
438 .and_then(|v| Rgba::try_from(v.as_str()?).ok())
439 .map(|s| s.to_owned());
440 let background = v
441 .get("background")
442 .and_then(|v| Rgba::try_from(v.as_str()?).ok())
443 .map(|s| s.to_owned());
444
445 Some(SemanticTokenRule {
446 token_type: Some(k.clone()),
447 token_modifiers: vec![],
448 style: vec![],
449 underline: if underline {
450 Some(SemanticTokenColorOverride::InheritForeground(true))
451 } else {
452 None
453 },
454 strikethrough: if strikethrough {
455 Some(SemanticTokenColorOverride::InheritForeground(true))
456 } else {
457 None
458 },
459 foreground_color: foreground,
460 background_color: background,
461 font_weight,
462 font_style,
463 })
464 })
465 .collect(),
466 )
467 })
468 .unwrap_or_default(),
469 })
470 }
471
472 fn minimap_content(&self) -> Option<MinimapContent> {
473 let minimap_enabled = self.read_bool("editor.minimap.enabled");
474 let autohide = self.read_bool("editor.minimap.autohide");
475 let show = match (minimap_enabled, autohide) {
476 (Some(true), Some(false)) => Some(ShowMinimap::Always),
477 (Some(true), _) => Some(ShowMinimap::Auto),
478 (Some(false), _) => Some(ShowMinimap::Never),
479 _ => None,
480 };
481
482 skip_default(MinimapContent {
483 show,
484 thumb: self.read_enum("editor.minimap.showSlider", |s| match s {
485 "always" => Some(MinimapThumb::Always),
486 "mouseover" => Some(MinimapThumb::Hover),
487 _ => None,
488 }),
489 max_width_columns: self
490 .read_u32("editor.minimap.maxColumn")
491 .and_then(|v| NonZeroU32::new(v)),
492 ..Default::default()
493 })
494 }
495
496 fn git_panel_settings_content(&self) -> Option<GitPanelSettingsContent> {
497 skip_default(GitPanelSettingsContent {
498 button: self.read_bool("git.enabled"),
499 fallback_branch_name: self.read_string("git.defaultBranchName"),
500 ..Default::default()
501 })
502 }
503
504 fn project_settings_content(&self) -> ProjectSettingsContent {
505 ProjectSettingsContent {
506 all_languages: AllLanguageSettingsContent {
507 edit_predictions: self.edit_predictions_settings_content(),
508 defaults: self.default_language_settings_content(),
509 languages: Default::default(),
510 file_types: self.file_types(),
511 },
512 worktree: self.worktree_settings_content(),
513 lsp: Default::default(),
514 terminal: None,
515 dap: Default::default(),
516 context_servers: self.context_servers(),
517 context_server_timeout: None,
518 load_direnv: None,
519 git_hosting_providers: None,
520 disable_ai: None,
521 }
522 }
523
524 fn default_language_settings_content(&self) -> LanguageSettingsContent {
525 LanguageSettingsContent {
526 allow_rewrap: None,
527 always_treat_brackets_as_autoclosed: None,
528 auto_indent: None,
529 auto_indent_on_paste: self.read_bool("editor.formatOnPaste"),
530 code_actions_on_format: None,
531 completions: skip_default(CompletionSettingsContent {
532 words: self.read_bool("editor.suggest.showWords").map(|b| {
533 if b {
534 WordsCompletionMode::Enabled
535 } else {
536 WordsCompletionMode::Disabled
537 }
538 }),
539 ..Default::default()
540 }),
541 debuggers: None,
542 edit_predictions_disabled_in: None,
543 enable_language_server: None,
544 ensure_final_newline_on_save: self.read_bool("files.insertFinalNewline"),
545 line_ending: self.read_enum("files.eol", |s| match s {
546 "\n" => Some(LineEndingSetting::PreferLf),
547 "\r\n" => Some(LineEndingSetting::PreferCrlf),
548 "auto" => Some(LineEndingSetting::Detect),
549 _ => None,
550 }),
551 extend_comment_on_newline: None,
552 extend_list_on_newline: None,
553 indent_list_on_tab: None,
554 format_on_save: self.read_bool("editor.guides.formatOnSave").map(|b| {
555 if b {
556 FormatOnSave::On
557 } else {
558 FormatOnSave::Off
559 }
560 }),
561 formatter: None,
562 hard_tabs: self.read_bool("editor.insertSpaces").map(|v| !v),
563 indent_guides: skip_default(IndentGuideSettingsContent {
564 enabled: self.read_bool("editor.guides.indentation"),
565 ..Default::default()
566 }),
567 inlay_hints: None,
568 jsx_tag_auto_close: None,
569 language_servers: None,
570 semantic_tokens: self
571 .read_bool("editor.semanticHighlighting.enabled")
572 .map(|enabled| {
573 if enabled {
574 SemanticTokens::Full
575 } else {
576 SemanticTokens::Off
577 }
578 }),
579 document_folding_ranges: None,
580 document_symbols: None,
581 linked_edits: self.read_bool("editor.linkedEditing"),
582 preferred_line_length: self.read_u32("editor.wordWrapColumn"),
583 prettier: None,
584 remove_trailing_whitespace_on_save: self.read_bool("editor.trimAutoWhitespace"),
585 show_completion_documentation: None,
586 colorize_brackets: self.read_bool("editor.bracketPairColorization.enabled"),
587 show_completions_on_input: self.read_bool("editor.suggestOnTriggerCharacters"),
588 show_edit_predictions: self.read_bool("editor.inlineSuggest.enabled"),
589 show_whitespaces: self.read_enum("editor.renderWhitespace", |s| {
590 Some(match s {
591 "boundary" => ShowWhitespaceSetting::Boundary,
592 "trailing" => ShowWhitespaceSetting::Trailing,
593 "selection" => ShowWhitespaceSetting::Selection,
594 "all" => ShowWhitespaceSetting::All,
595 _ => ShowWhitespaceSetting::None,
596 })
597 }),
598 show_wrap_guides: None,
599 soft_wrap: self.read_enum("editor.wordWrap", |s| match s {
600 "on" => Some(SoftWrap::EditorWidth),
601 "wordWrapColumn" => Some(SoftWrap::PreferLine),
602 "bounded" => Some(SoftWrap::Bounded),
603 "off" => Some(SoftWrap::None),
604 _ => None,
605 }),
606 tab_size: self
607 .read_u32("editor.tabSize")
608 .and_then(|n| NonZeroU32::new(n)),
609 tasks: None,
610 use_auto_surround: self.read_enum("editor.autoSurround", |s| match s {
611 "languageDefined" | "quotes" | "brackets" => Some(true),
612 "never" => Some(false),
613 _ => None,
614 }),
615 use_autoclose: None,
616 use_on_type_format: self.read_bool("editor.formatOnType"),
617 whitespace_map: None,
618 wrap_guides: self
619 .read_value("editor.rulers")
620 .and_then(|v| v.as_array())
621 .map(|v| {
622 v.iter()
623 .flat_map(|n| n.as_u64().map(|n| n as usize))
624 .collect()
625 }),
626 word_diff_enabled: None,
627 }
628 }
629
630 fn file_types(&self) -> Option<HashMap<Arc<str>, ExtendingVec<String>>> {
631 // vscodes file association map is inverted from ours, so we flip the mapping before merging
632 let mut associations: HashMap<Arc<str>, ExtendingVec<String>> = HashMap::default();
633 let map = self.read_value("files.associations")?.as_object()?;
634 for (k, v) in map {
635 let Some(v) = v.as_str() else { continue };
636 associations.entry(v.into()).or_default().0.push(k.clone());
637 }
638 skip_default(associations)
639 }
640
641 fn edit_predictions_settings_content(&self) -> Option<EditPredictionSettingsContent> {
642 let disabled_globs = self
643 .read_value("cursor.general.globalCursorIgnoreList")?
644 .as_array()?;
645
646 skip_default(EditPredictionSettingsContent {
647 disabled_globs: skip_default(
648 disabled_globs
649 .iter()
650 .filter_map(|glob| glob.as_str())
651 .map(|s| s.to_string())
652 .collect(),
653 ),
654 ..Default::default()
655 })
656 }
657
658 fn outline_panel_settings_content(&self) -> Option<OutlinePanelSettingsContent> {
659 skip_default(OutlinePanelSettingsContent {
660 file_icons: self.read_bool("outline.icons"),
661 folder_icons: self.read_bool("outline.icons"),
662 git_status: self.read_bool("git.decorations.enabled"),
663 ..Default::default()
664 })
665 }
666
667 fn node_binary_settings(&self) -> Option<NodeBinarySettings> {
668 // this just sets the binary name instead of a full path so it relies on path lookup
669 // resolving to the one you want
670 skip_default(NodeBinarySettings {
671 npm_path: self.read_enum("npm.packageManager", |s| match s {
672 v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
673 _ => None,
674 }),
675 ..Default::default()
676 })
677 }
678
679 fn git_settings_content(&self) -> Option<GitSettings> {
680 let inline_blame = self.read_bool("git.blame.editorDecoration.enabled")?;
681 skip_default(GitSettings {
682 inline_blame: Some(InlineBlameSettings {
683 enabled: Some(inline_blame),
684 ..Default::default()
685 }),
686 ..Default::default()
687 })
688 }
689
690 fn context_servers(&self) -> HashMap<Arc<str>, ContextServerSettingsContent> {
691 #[derive(Deserialize)]
692 struct VsCodeContextServerCommand {
693 command: PathBuf,
694 args: Option<Vec<String>>,
695 env: Option<HashMap<String, String>>,
696 // note: we don't support envFile and type
697 }
698 let Some(mcp) = self.read_value("mcp").and_then(|v| v.as_object()) else {
699 return Default::default();
700 };
701 mcp.iter()
702 .filter_map(|(k, v)| {
703 Some((
704 k.clone().into(),
705 ContextServerSettingsContent::Stdio {
706 enabled: true,
707 remote: false,
708 command: serde_json::from_value::<VsCodeContextServerCommand>(v.clone())
709 .ok()
710 .map(|cmd| ContextServerCommand {
711 path: cmd.command,
712 args: cmd.args.unwrap_or_default(),
713 env: cmd.env,
714 timeout: None,
715 })?,
716 },
717 ))
718 })
719 .collect()
720 }
721
722 fn item_settings_content(&self) -> Option<ItemSettingsContent> {
723 skip_default(ItemSettingsContent {
724 git_status: self.read_bool("git.decorations.enabled"),
725 close_position: self.read_enum("workbench.editor.tabActionLocation", |s| match s {
726 "right" => Some(ClosePosition::Right),
727 "left" => Some(ClosePosition::Left),
728 _ => None,
729 }),
730 file_icons: self.read_bool("workbench.editor.showIcons"),
731 activate_on_close: self
732 .read_bool("workbench.editor.focusRecentEditorAfterClose")
733 .map(|b| {
734 if b {
735 ActivateOnClose::History
736 } else {
737 ActivateOnClose::LeftNeighbour
738 }
739 }),
740 show_diagnostics: None,
741 show_close_button: self
742 .read_bool("workbench.editor.tabActionCloseVisibility")
743 .map(|b| {
744 if b {
745 ShowCloseButton::Always
746 } else {
747 ShowCloseButton::Hidden
748 }
749 }),
750 })
751 }
752
753 fn preview_tabs_settings_content(&self) -> Option<PreviewTabsSettingsContent> {
754 skip_default(PreviewTabsSettingsContent {
755 enabled: self.read_bool("workbench.editor.enablePreview"),
756 enable_preview_from_project_panel: None,
757 enable_preview_from_file_finder: self
758 .read_bool("workbench.editor.enablePreviewFromQuickOpen"),
759 enable_preview_from_multibuffer: None,
760 enable_preview_multibuffer_from_code_navigation: None,
761 enable_preview_file_from_code_navigation: None,
762 enable_keep_preview_on_code_navigation: self
763 .read_bool("workbench.editor.enablePreviewFromCodeNavigation"),
764 })
765 }
766
767 fn tab_bar_settings_content(&self) -> Option<TabBarSettingsContent> {
768 skip_default(TabBarSettingsContent {
769 show: self.read_enum("workbench.editor.showTabs", |s| match s {
770 "multiple" => Some(true),
771 "single" | "none" => Some(false),
772 _ => None,
773 }),
774 show_nav_history_buttons: None,
775 show_tab_bar_buttons: self
776 .read_str("workbench.editor.editorActionsLocation")
777 .and_then(|str| if str == "hidden" { Some(false) } else { None }),
778 show_pinned_tabs_in_separate_row: None,
779 })
780 }
781
782 fn status_bar_settings_content(&self) -> Option<StatusBarSettingsContent> {
783 skip_default(StatusBarSettingsContent {
784 show: self.read_bool("workbench.statusBar.visible"),
785 show_active_file: None,
786 active_language_button: None,
787 cursor_position_button: None,
788 line_endings_button: None,
789 active_encoding_button: None,
790 })
791 }
792
793 fn project_panel_settings_content(&self) -> Option<ProjectPanelSettingsContent> {
794 let mut project_panel_settings = ProjectPanelSettingsContent {
795 auto_fold_dirs: self.read_bool("explorer.compactFolders"),
796 auto_reveal_entries: self.read_bool("explorer.autoReveal"),
797 bold_folder_labels: None,
798 button: None,
799 default_width: None,
800 dock: None,
801 drag_and_drop: None,
802 entry_spacing: None,
803 file_icons: None,
804 folder_icons: None,
805 git_status: self.read_bool("git.decorations.enabled"),
806 hide_gitignore: self.read_bool("explorer.excludeGitIgnore"),
807 hide_hidden: None,
808 hide_root: None,
809 indent_guides: None,
810 indent_size: None,
811 scrollbar: self.read_bool("workbench.list.horizontalScrolling").map(
812 |horizontal_scrolling| ProjectPanelScrollbarSettingsContent {
813 show: None,
814 horizontal_scroll: Some(horizontal_scrolling),
815 },
816 ),
817 show_diagnostics: self
818 .read_bool("problems.decorations.enabled")
819 .and_then(|b| if b { Some(ShowDiagnostics::Off) } else { None }),
820 sort_mode: self.read_enum("explorer.sortOrder", |s| match s {
821 "default" | "foldersNestsFiles" => Some(ProjectPanelSortMode::DirectoriesFirst),
822 "mixed" => Some(ProjectPanelSortMode::Mixed),
823 "filesFirst" => Some(ProjectPanelSortMode::FilesFirst),
824 _ => None,
825 }),
826 sort_order: self.read_enum("explorer.sortOrderLexicographicOptions", |s| match s {
827 "default" => Some(ProjectPanelSortOrder::Default),
828 "upper" => Some(ProjectPanelSortOrder::Upper),
829 "lower" => Some(ProjectPanelSortOrder::Lower),
830 "unicode" => Some(ProjectPanelSortOrder::Unicode),
831 _ => None,
832 }),
833 starts_open: None,
834 sticky_scroll: None,
835 auto_open: None,
836 diagnostic_badges: None,
837 git_status_indicator: None,
838 };
839
840 if let (Some(false), Some(false)) = (
841 self.read_bool("explorer.decorations.badges"),
842 self.read_bool("explorer.decorations.colors"),
843 ) {
844 project_panel_settings.git_status = Some(false);
845 project_panel_settings.show_diagnostics = Some(ShowDiagnostics::Off);
846 }
847
848 skip_default(project_panel_settings)
849 }
850
851 fn telemetry_settings_content(&self) -> Option<TelemetrySettingsContent> {
852 self.read_enum("telemetry.telemetryLevel", |level| {
853 let (metrics, diagnostics) = match level {
854 "all" => (true, true),
855 "error" | "crash" => (false, true),
856 "off" => (false, false),
857 _ => return None,
858 };
859 Some(TelemetrySettingsContent {
860 metrics: Some(metrics),
861 diagnostics: Some(diagnostics),
862 })
863 })
864 }
865
866 fn terminal_settings_content(&self) -> Option<TerminalSettingsContent> {
867 let (font_family, font_fallbacks) = self.read_fonts("terminal.integrated.fontFamily");
868 skip_default(TerminalSettingsContent {
869 alternate_scroll: None,
870 blinking: self
871 .read_bool("terminal.integrated.cursorBlinking")
872 .map(|b| {
873 if b {
874 TerminalBlink::On
875 } else {
876 TerminalBlink::Off
877 }
878 }),
879 button: None,
880 copy_on_select: self.read_bool("terminal.integrated.copyOnSelection"),
881 cursor_shape: self.read_enum("terminal.integrated.cursorStyle", |s| match s {
882 "block" => Some(CursorShapeContent::Block),
883 "line" => Some(CursorShapeContent::Bar),
884 "underline" => Some(CursorShapeContent::Underline),
885 _ => None,
886 }),
887 default_height: None,
888 default_width: None,
889 dock: None,
890 font_fallbacks,
891 font_family,
892 font_features: None,
893 font_size: self
894 .read_f32("terminal.integrated.fontSize")
895 .map(FontSize::from),
896 font_weight: None,
897 keep_selection_on_copy: None,
898 line_height: self
899 .read_f32("terminal.integrated.lineHeight")
900 .map(|lh| TerminalLineHeight::Custom(lh)),
901 max_scroll_history_lines: self.read_usize("terminal.integrated.scrollback"),
902 bell: self
903 .read_value("accessibility.signals.terminalBell")
904 .and_then(|v| Some(v.get("sound")?.as_str()? == "on"))
905 .or_else(|| {
906 // Older deprecated setting, might as well still support it:
907 self.read_value("terminal.integrated.enableBell")
908 .map(|v| v.as_bool() == Some(true) || v.as_str() == Some("both"))
909 })
910 .map(|enabled| {
911 if enabled {
912 TerminalBell::System
913 } else {
914 TerminalBell::Off
915 }
916 }),
917 minimum_contrast: None,
918 option_as_meta: self.read_bool("terminal.integrated.macOptionIsMeta"),
919 project: self.project_terminal_settings_content(),
920 scrollbar: None,
921 scroll_multiplier: None,
922 toolbar: None,
923 show_count_badge: None,
924 flexible: None,
925 })
926 }
927
928 fn project_terminal_settings_content(&self) -> ProjectTerminalSettingsContent {
929 #[cfg(target_os = "windows")]
930 let platform = "windows";
931 #[cfg(target_os = "linux")]
932 let platform = "linux";
933 #[cfg(target_os = "macos")]
934 let platform = "osx";
935 #[cfg(target_os = "freebsd")]
936 let platform = "freebsd";
937 let env = self
938 .read_value(&format!("terminal.integrated.env.{platform}"))
939 .and_then(|v| v.as_object())
940 .map(|v| {
941 v.iter()
942 .map(|(k, v)| (k.clone(), v.to_string()))
943 // zed does not support substitutions, so this can break env vars
944 .filter(|(_, v)| !v.contains('$'))
945 .collect()
946 });
947
948 ProjectTerminalSettingsContent {
949 // TODO: handle arguments
950 shell: self
951 .read_string(&format!("terminal.integrated.{platform}Exec"))
952 .map(|s| Shell::Program(s)),
953 working_directory: None,
954 env,
955 detect_venv: None,
956 path_hyperlink_regexes: None,
957 path_hyperlink_timeout_ms: None,
958 }
959 }
960
961 fn theme_settings_content(&self) -> ThemeSettingsContent {
962 let (buffer_font_family, buffer_font_fallbacks) = self.read_fonts("editor.fontFamily");
963 ThemeSettingsContent {
964 ui_font_size: None,
965 ui_font_family: None,
966 ui_font_fallbacks: None,
967 ui_font_features: None,
968 ui_font_weight: None,
969 buffer_font_family,
970 buffer_font_fallbacks,
971 buffer_font_size: self.read_f32("editor.fontSize").map(FontSize::from),
972 buffer_font_weight: self.read_f32("editor.fontWeight").map(FontWeightContent),
973 buffer_line_height: None,
974 buffer_font_features: None,
975 agent_ui_font_size: None,
976 agent_buffer_font_size: None,
977 theme: None,
978 icon_theme: None,
979 ui_density: None,
980 unnecessary_code_fade: None,
981 experimental_theme_overrides: None,
982 theme_overrides: Default::default(),
983 }
984 }
985
986 fn workspace_settings_content(&self) -> WorkspaceSettingsContent {
987 WorkspaceSettingsContent {
988 active_pane_modifiers: self.active_pane_modifiers(),
989 text_rendering_mode: None,
990 autosave: self.read_enum("files.autoSave", |s| match s {
991 "off" => Some(AutosaveSetting::Off),
992 "afterDelay" => Some(AutosaveSetting::AfterDelay {
993 milliseconds: self
994 .read_value("files.autoSaveDelay")
995 .and_then(|v| v.as_u64())
996 .unwrap_or(1000)
997 .into(),
998 }),
999 "onFocusChange" => Some(AutosaveSetting::OnFocusChange),
1000 "onWindowChange" => Some(AutosaveSetting::OnWindowChange),
1001 _ => None,
1002 }),
1003 bottom_dock_layout: None,
1004 centered_layout: None,
1005 cli_default_open_behavior: None,
1006 close_on_file_delete: None,
1007 close_panel_on_toggle: None,
1008 command_aliases: Default::default(),
1009 confirm_quit: self.read_enum("window.confirmBeforeClose", |s| match s {
1010 "always" | "keyboardOnly" => Some(true),
1011 "never" => Some(false),
1012 _ => None,
1013 }),
1014 drop_target_size: None,
1015 // workbench.editor.limit contains "enabled", "value", and "perEditorGroup"
1016 // our semantics match if those are set to true, some N, and true respectively.
1017 // we'll ignore "perEditorGroup" for now since we only support a global max
1018 max_tabs: if self.read_bool("workbench.editor.limit.enabled") == Some(true) {
1019 self.read_usize("workbench.editor.limit.value")
1020 .and_then(|n| NonZeroUsize::new(n))
1021 } else {
1022 None
1023 },
1024 on_last_window_closed: None,
1025 pane_split_direction_horizontal: None,
1026 pane_split_direction_vertical: None,
1027 resize_all_panels_in_dock: None,
1028 restore_on_file_reopen: self.read_bool("workbench.editor.restoreViewState"),
1029 restore_on_startup: None,
1030 window_decorations: None,
1031 show_call_status_icon: None,
1032 use_system_path_prompts: self.read_bool("files.simpleDialog.enable"),
1033 use_system_prompts: None,
1034 use_system_window_tabs: self.read_bool("window.nativeTabs"),
1035 when_closing_with_no_tabs: self.read_bool("window.closeWhenEmpty").map(|b| {
1036 if b {
1037 CloseWindowWhenNoItems::CloseWindow
1038 } else {
1039 CloseWindowWhenNoItems::KeepWindowOpen
1040 }
1041 }),
1042 zoomed_padding: None,
1043 focus_follows_mouse: None,
1044 }
1045 }
1046
1047 fn active_pane_modifiers(&self) -> Option<ActivePaneModifiers> {
1048 if self.read_bool("accessibility.dimUnfocused.enabled") == Some(true)
1049 && let Some(opacity) = self.read_f32("accessibility.dimUnfocused.opacity")
1050 {
1051 Some(ActivePaneModifiers {
1052 border_size: None,
1053 inactive_opacity: Some(InactiveOpacity(opacity)),
1054 })
1055 } else {
1056 None
1057 }
1058 }
1059
1060 fn worktree_settings_content(&self) -> WorktreeSettingsContent {
1061 WorktreeSettingsContent {
1062 prevent_sharing_in_public_channels: false,
1063 file_scan_exclusions: self
1064 .read_value("files.watcherExclude")
1065 .and_then(|v| v.as_array())
1066 .map(|v| {
1067 v.iter()
1068 .filter_map(|n| n.as_str().map(str::to_owned))
1069 .collect::<Vec<_>>()
1070 })
1071 .filter(|r| !r.is_empty()),
1072 file_scan_inclusions: self
1073 .read_value("files.watcherInclude")
1074 .and_then(|v| v.as_array())
1075 .map(|v| {
1076 v.iter()
1077 .filter_map(|n| n.as_str().map(str::to_owned))
1078 .collect::<Vec<_>>()
1079 })
1080 .filter(|r| !r.is_empty()),
1081 private_files: None,
1082 hidden_files: None,
1083 read_only_files: self
1084 .read_value("files.readonlyExclude")
1085 .and_then(|v| v.as_object())
1086 .map(|v| {
1087 v.iter()
1088 .filter_map(|(k, v)| {
1089 if v.as_bool().unwrap_or(false) {
1090 Some(k.to_owned())
1091 } else {
1092 None
1093 }
1094 })
1095 .collect::<Vec<_>>()
1096 })
1097 .filter(|r| !r.is_empty()),
1098 }
1099 }
1100}
1101
1102fn skip_default<T: Default + PartialEq>(value: T) -> Option<T> {
1103 if value == T::default() {
1104 None
1105 } else {
1106 Some(value)
1107 }
1108}