vscode_import.rs

   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}