Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:55:05.459Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

vscode_import.rs

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