vscode_import.rs

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