use crate::*;use anyhow::{Context as _, Result, anyhow};use collections::HashMap;use fs::Fs;use paths::{cursor_settings_file_paths, vscode_settings_file_paths};use serde::Deserialize;use serde_json::{Map, Value};use std::{ num::{NonZeroU32, NonZeroUsize}, path::{Path, PathBuf}, sync::Arc,}; #[derive(Clone, Copy, PartialEq, Eq, Debug)]pub enum VsCodeSettingsSource { VsCode, Cursor,} impl std::fmt::Display for VsCodeSettingsSource { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { VsCodeSettingsSource::VsCode => write!(f, "VS Code"), VsCodeSettingsSource::Cursor => write!(f, "Cursor"), } }} pub struct VsCodeSettings { pub source: VsCodeSettingsSource, pub path: Arc<Path>, content: Map<String, Value>,} impl VsCodeSettings { #[cfg(any(test, feature = "test-support"))] pub fn from_str(content: &str, source: VsCodeSettingsSource) -> Result<Self> { Ok(Self { source, path: Path::new("/example-path/Code/User/settings.json").into(), content: serde_json_lenient::from_str(content)?, }) } pub async fn load_user_settings(source: VsCodeSettingsSource, fs: Arc<dyn Fs>) -> Result<Self> { let candidate_paths = match source { VsCodeSettingsSource::VsCode => vscode_settings_file_paths(), VsCodeSettingsSource::Cursor => cursor_settings_file_paths(), }; let mut path = None; for candidate_path in candidate_paths.iter() { if fs.is_file(candidate_path).await { path = Some(candidate_path.clone()); } } let Some(path) = path else { return Err(anyhow!( "No settings file found, expected to find it in one of the following paths:\n{}", candidate_paths .into_iter() .map(|path| path.to_string_lossy().into_owned()) .collect::<Vec<_>>() .join("\n") )); }; let content = fs.load(&path).await.with_context(|| { format!( "Error loading {} settings file from {}", source, path.display() ) })?; let content = serde_json_lenient::from_str(&content).with_context(|| { format!( "Error parsing {} settings file from {}", source, path.display() ) })?; Ok(Self { source, path: path.into(), content, }) } fn read_value(&self, setting: &str) -> Option<&Value> { self.content.get(setting) } fn read_str(&self, setting: &str) -> Option<&str> { self.read_value(setting).and_then(|v| v.as_str()) } fn read_string(&self, setting: &str) -> Option<String> { self.read_value(setting) .and_then(|v| v.as_str()) .map(|s| s.to_owned()) } fn read_bool(&self, setting: &str) -> Option<bool> { self.read_value(setting).and_then(|v| v.as_bool()) } fn read_f32(&self, setting: &str) -> Option<f32> { self.read_value(setting) .and_then(|v| v.as_f64()) .map(|v| v as f32) } fn read_u64(&self, setting: &str) -> Option<u64> { self.read_value(setting).and_then(|v| v.as_u64()) } fn read_usize(&self, setting: &str) -> Option<usize> { self.read_value(setting) .and_then(|v| v.as_u64()) .and_then(|v| v.try_into().ok()) } fn read_u32(&self, setting: &str) -> Option<u32> { self.read_value(setting) .and_then(|v| v.as_u64()) .and_then(|v| v.try_into().ok()) } fn read_enum<T>(&self, key: &str, f: impl FnOnce(&str) -> Option<T>) -> Option<T> { self.content.get(key).and_then(Value::as_str).and_then(f) } fn read_fonts(&self, key: &str) -> (Option<FontFamilyName>, Option<Vec<FontFamilyName>>) { let Some(css_name) = self.content.get(key).and_then(Value::as_str) else { return (None, None); }; let mut name_buffer = String::new(); let mut quote_char: Option<char> = None; let mut fonts = Vec::new(); let mut add_font = |buffer: &mut String| { let trimmed = buffer.trim(); if !trimmed.is_empty() { fonts.push(trimmed.to_string().into()); } buffer.clear(); }; for ch in css_name.chars() { match (ch, quote_char) { ('"' | '\'', None) => { quote_char = Some(ch); } (_, Some(q)) if ch == q => { quote_char = None; } (',', None) => { add_font(&mut name_buffer); } _ => { name_buffer.push(ch); } } } add_font(&mut name_buffer); if fonts.is_empty() { return (None, None); } (Some(fonts.remove(0)), skip_default(fonts)) } pub fn settings_content(&self) -> SettingsContent { SettingsContent { agent: self.agent_settings_content(), agent_servers: None, audio: None, auto_update: None, base_keymap: Some(BaseKeymapContent::VSCode), calls: None, collaboration_panel: None, debugger: None, diagnostics: None, disable_ai: None, editor: self.editor_settings_content(), extension: ExtensionSettingsContent::default(), file_finder: None, git: self.git_settings_content(), git_panel: self.git_panel_settings_content(), global_lsp_settings: None, helix_mode: None, image_viewer: None, journal: None, language_models: None, line_indicator_format: None, log: None, message_editor: None, node: self.node_binary_settings(), notification_panel: None, outline_panel: self.outline_panel_settings_content(), preview_tabs: self.preview_tabs_settings_content(), project: self.project_settings_content(), project_panel: self.project_panel_settings_content(), proxy: self.read_string("http.proxy"), remote: RemoteSettingsContent::default(), repl: None, server_url: None, session: None, status_bar: self.status_bar_settings_content(), tab_bar: self.tab_bar_settings_content(), tabs: self.item_settings_content(), telemetry: self.telemetry_settings_content(), terminal: self.terminal_settings_content(), theme: Box::new(self.theme_settings_content()), title_bar: None, vim: None, vim_mode: None, workspace: self.workspace_settings_content(), } } fn agent_settings_content(&self) -> Option<AgentSettingsContent> { let enabled = self.read_bool("chat.agent.enabled"); skip_default(AgentSettingsContent { enabled: enabled, button: enabled, ..Default::default() }) } fn editor_settings_content(&self) -> EditorSettingsContent { EditorSettingsContent { auto_signature_help: self.read_bool("editor.parameterHints.enabled"), autoscroll_on_clicks: None, cursor_blink: self.read_enum("editor.cursorBlinking", |s| match s { "blink" | "phase" | "expand" | "smooth" => Some(true), "solid" => Some(false), _ => None, }), cursor_shape: self.read_enum("editor.cursorStyle", |s| match s { "block" => Some(CursorShape::Block), "block-outline" => Some(CursorShape::Hollow), "line" | "line-thin" => Some(CursorShape::Bar), "underline" | "underline-thin" => Some(CursorShape::Underline), _ => None, }), current_line_highlight: self.read_enum("editor.renderLineHighlight", |s| match s { "gutter" => Some(CurrentLineHighlight::Gutter), "line" => Some(CurrentLineHighlight::Line), "all" => Some(CurrentLineHighlight::All), _ => None, }), diagnostics_max_severity: None, double_click_in_multibuffer: None, drag_and_drop_selection: None, excerpt_context_lines: None, expand_excerpt_lines: None, fast_scroll_sensitivity: self.read_f32("editor.fastScrollSensitivity"), go_to_definition_fallback: None, gutter: self.gutter_content(), hide_mouse: None, horizontal_scroll_margin: None, hover_popover_delay: self.read_u64("editor.hover.delay").map(Into::into), hover_popover_enabled: self.read_bool("editor.hover.enabled"), inline_code_actions: None, jupyter: None, lsp_document_colors: None, lsp_highlight_debounce: None, middle_click_paste: None, minimap: self.minimap_content(), minimum_contrast_for_highlights: None, multi_cursor_modifier: self.read_enum("editor.multiCursorModifier", |s| match s { "ctrlCmd" => Some(MultiCursorModifier::CmdOrCtrl), "alt" => Some(MultiCursorModifier::Alt), _ => None, }), redact_private_values: None, relative_line_numbers: self.read_enum("editor.lineNumbers", |s| match s { "relative" => Some(RelativeLineNumbers::Enabled), _ => None, }), rounded_selection: self.read_bool("editor.roundedSelection"), scroll_beyond_last_line: None, scroll_sensitivity: self.read_f32("editor.mouseWheelScrollSensitivity"), scrollbar: self.scrollbar_content(), search: self.search_content(), search_wrap: None, seed_search_query_from_cursor: self.read_enum( "editor.find.seedSearchStringFromSelection", |s| match s { "always" => Some(SeedQuerySetting::Always), "selection" => Some(SeedQuerySetting::Selection), "never" => Some(SeedQuerySetting::Never), _ => None, }, ), selection_highlight: self.read_bool("editor.selectionHighlight"), show_signature_help_after_edits: self.read_bool("editor.parameterHints.enabled"), snippet_sort_order: None, toolbar: None, use_smartcase_search: self.read_bool("search.smartCase"), vertical_scroll_margin: self.read_f32("editor.cursorSurroundingLines"), completion_menu_scrollbar: None, } } fn gutter_content(&self) -> Option<GutterContent> { skip_default(GutterContent { line_numbers: self.read_enum("editor.lineNumbers", |s| match s { "on" | "relative" => Some(true), "off" => Some(false), _ => None, }), min_line_number_digits: None, runnables: None, breakpoints: None, folds: self.read_enum("editor.showFoldingControls", |s| match s { "always" | "mouseover" => Some(true), "never" => Some(false), _ => None, }), }) } fn scrollbar_content(&self) -> Option<ScrollbarContent> { let scrollbar_axes = skip_default(ScrollbarAxesContent { horizontal: self.read_enum("editor.scrollbar.horizontal", |s| match s { "auto" | "visible" => Some(true), "hidden" => Some(false), _ => None, }), vertical: self.read_enum("editor.scrollbar.vertical", |s| match s { "auto" | "visible" => Some(true), "hidden" => Some(false), _ => None, }), })?; Some(ScrollbarContent { axes: Some(scrollbar_axes), ..Default::default() }) } fn search_content(&self) -> Option<SearchSettingsContent> { skip_default(SearchSettingsContent { include_ignored: self.read_bool("search.useIgnoreFiles"), ..Default::default() }) } fn minimap_content(&self) -> Option<MinimapContent> { let minimap_enabled = self.read_bool("editor.minimap.enabled"); let autohide = self.read_bool("editor.minimap.autohide"); let show = match (minimap_enabled, autohide) { (Some(true), Some(false)) => Some(ShowMinimap::Always), (Some(true), _) => Some(ShowMinimap::Auto), (Some(false), _) => Some(ShowMinimap::Never), _ => None, }; skip_default(MinimapContent { show, thumb: self.read_enum("editor.minimap.showSlider", |s| match s { "always" => Some(MinimapThumb::Always), "mouseover" => Some(MinimapThumb::Hover), _ => None, }), max_width_columns: self .read_u32("editor.minimap.maxColumn") .and_then(|v| NonZeroU32::new(v)), ..Default::default() }) } fn git_panel_settings_content(&self) -> Option<GitPanelSettingsContent> { skip_default(GitPanelSettingsContent { button: self.read_bool("git.enabled"), fallback_branch_name: self.read_string("git.defaultBranchName"), ..Default::default() }) } fn project_settings_content(&self) -> ProjectSettingsContent { ProjectSettingsContent { all_languages: AllLanguageSettingsContent { features: None, edit_predictions: self.edit_predictions_settings_content(), defaults: self.default_language_settings_content(), languages: Default::default(), file_types: self.file_types(), }, worktree: self.worktree_settings_content(), lsp: Default::default(), terminal: None, dap: Default::default(), context_servers: self.context_servers(), load_direnv: None, slash_commands: None, git_hosting_providers: None, } } fn default_language_settings_content(&self) -> LanguageSettingsContent { LanguageSettingsContent { allow_rewrap: None, always_treat_brackets_as_autoclosed: None, auto_indent: None, auto_indent_on_paste: self.read_bool("editor.formatOnPaste"), code_actions_on_format: None, completions: skip_default(CompletionSettingsContent { words: self.read_bool("editor.suggest.showWords").map(|b| { if b { WordsCompletionMode::Enabled } else { WordsCompletionMode::Disabled } }), ..Default::default() }), debuggers: None, edit_predictions_disabled_in: None, enable_language_server: None, ensure_final_newline_on_save: self.read_bool("files.insertFinalNewline"), extend_comment_on_newline: None, format_on_save: self.read_bool("editor.guides.formatOnSave").map(|b| { if b { FormatOnSave::On } else { FormatOnSave::Off } }), formatter: None, hard_tabs: self.read_bool("editor.insertSpaces").map(|v| !v), indent_guides: skip_default(IndentGuideSettingsContent { enabled: self.read_bool("editor.guides.indentation"), ..Default::default() }), inlay_hints: None, jsx_tag_auto_close: None, language_servers: None, linked_edits: self.read_bool("editor.linkedEditing"), preferred_line_length: self.read_u32("editor.wordWrapColumn"), prettier: None, remove_trailing_whitespace_on_save: self.read_bool("editor.trimAutoWhitespace"), show_completion_documentation: None, show_completions_on_input: self.read_bool("editor.suggestOnTriggerCharacters"), show_edit_predictions: self.read_bool("editor.inlineSuggest.enabled"), show_whitespaces: self.read_enum("editor.renderWhitespace", |s| { Some(match s { "boundary" => ShowWhitespaceSetting::Boundary, "trailing" => ShowWhitespaceSetting::Trailing, "selection" => ShowWhitespaceSetting::Selection, "all" => ShowWhitespaceSetting::All, _ => ShowWhitespaceSetting::None, }) }), show_wrap_guides: None, soft_wrap: self.read_enum("editor.wordWrap", |s| match s { "on" => Some(SoftWrap::EditorWidth), "wordWrapColumn" => Some(SoftWrap::PreferLine), "bounded" => Some(SoftWrap::Bounded), "off" => Some(SoftWrap::None), _ => None, }), tab_size: self .read_u32("editor.tabSize") .and_then(|n| NonZeroU32::new(n)), tasks: None, use_auto_surround: self.read_enum("editor.autoSurround", |s| match s { "languageDefined" | "quotes" | "brackets" => Some(true), "never" => Some(false), _ => None, }), use_autoclose: None, use_on_type_format: self.read_bool("editor.formatOnType"), whitespace_map: None, wrap_guides: self .read_value("editor.rulers") .and_then(|v| v.as_array()) .map(|v| { v.iter() .flat_map(|n| n.as_u64().map(|n| n as usize)) .collect() }), } } fn file_types(&self) -> Option<HashMap<Arc<str>, ExtendingVec<String>>> { // vscodes file association map is inverted from ours, so we flip the mapping before merging let mut associations: HashMap<Arc<str>, ExtendingVec<String>> = HashMap::default(); let map = self.read_value("files.associations")?.as_object()?; for (k, v) in map { let Some(v) = v.as_str() else { continue }; associations.entry(v.into()).or_default().0.push(k.clone()); } skip_default(associations) } fn edit_predictions_settings_content(&self) -> Option<EditPredictionSettingsContent> { let disabled_globs = selfLines 1–500 of 890. Load more
use crate::*; use anyhow::{Context as _, Result, anyhow}; use collections::HashMap; use fs::Fs; use paths::{cursor_settings_file_paths, vscode_settings_file_paths}; use serde::Deserialize; use serde_json::{Map, Value}; use std::{ num::{NonZeroU32, NonZeroUsize}, path::{Path, PathBuf}, sync::Arc, }; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum VsCodeSettingsSource { VsCode, Cursor, } impl std::fmt::Display for VsCodeSettingsSource { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { VsCodeSettingsSource::VsCode => write!(f, "VS Code"), VsCodeSettingsSource::Cursor => write!(f, "Cursor"), } } } pub struct VsCodeSettings { pub source: VsCodeSettingsSource, pub path: Arc<Path>, content: Map<String, Value>, } impl VsCodeSettings { #[cfg(any(test, feature = "test-support"))] pub fn from_str(content: &str, source: VsCodeSettingsSource) -> Result<Self> { Ok(Self { source, path: Path::new("/example-path/Code/User/settings.json").into(), content: serde_json_lenient::from_str(content)?, }) } pub async fn load_user_settings(source: VsCodeSettingsSource, fs: Arc<dyn Fs>) -> Result<Self> { let candidate_paths = match source { VsCodeSettingsSource::VsCode => vscode_settings_file_paths(), VsCodeSettingsSource::Cursor => cursor_settings_file_paths(), }; let mut path = None; for candidate_path in candidate_paths.iter() { if fs.is_file(candidate_path).await { path = Some(candidate_path.clone()); } } let Some(path) = path else { return Err(anyhow!( "No settings file found, expected to find it in one of the following paths:\n{}", candidate_paths .into_iter() .map(|path| path.to_string_lossy().into_owned()) .collect::<Vec<_>>() .join("\n") )); }; let content = fs.load(&path).await.with_context(|| { format!( "Error loading {} settings file from {}", source, path.display() ) })?; let content = serde_json_lenient::from_str(&content).with_context(|| { format!( "Error parsing {} settings file from {}", source, path.display() ) })?; Ok(Self { source, path: path.into(), content, }) } fn read_value(&self, setting: &str) -> Option<&Value> { self.content.get(setting) } fn read_str(&self, setting: &str) -> Option<&str> { self.read_value(setting).and_then(|v| v.as_str()) } fn read_string(&self, setting: &str) -> Option<String> { self.read_value(setting) .and_then(|v| v.as_str()) .map(|s| s.to_owned()) } fn read_bool(&self, setting: &str) -> Option<bool> { self.read_value(setting).and_then(|v| v.as_bool()) } fn read_f32(&self, setting: &str) -> Option<f32> { self.read_value(setting) .and_then(|v| v.as_f64()) .map(|v| v as f32) } fn read_u64(&self, setting: &str) -> Option<u64> { self.read_value(setting).and_then(|v| v.as_u64()) } fn read_usize(&self, setting: &str) -> Option<usize> { self.read_value(setting) .and_then(|v| v.as_u64()) .and_then(|v| v.try_into().ok()) } fn read_u32(&self, setting: &str) -> Option<u32> { self.read_value(setting) .and_then(|v| v.as_u64()) .and_then(|v| v.try_into().ok()) } fn read_enum<T>(&self, key: &str, f: impl FnOnce(&str) -> Option<T>) -> Option<T> { self.content.get(key).and_then(Value::as_str).and_then(f) } fn read_fonts(&self, key: &str) -> (Option<FontFamilyName>, Option<Vec<FontFamilyName>>) { let Some(css_name) = self.content.get(key).and_then(Value::as_str) else { return (None, None); }; let mut name_buffer = String::new(); let mut quote_char: Option<char> = None; let mut fonts = Vec::new(); let mut add_font = |buffer: &mut String| { let trimmed = buffer.trim(); if !trimmed.is_empty() { fonts.push(trimmed.to_string().into()); } buffer.clear(); }; for ch in css_name.chars() { match (ch, quote_char) { ('"' | '\'', None) => { quote_char = Some(ch); } (_, Some(q)) if ch == q => { quote_char = None; } (',', None) => { add_font(&mut name_buffer); } _ => { name_buffer.push(ch); } } } add_font(&mut name_buffer); if fonts.is_empty() { return (None, None); } (Some(fonts.remove(0)), skip_default(fonts)) } pub fn settings_content(&self) -> SettingsContent { SettingsContent { agent: self.agent_settings_content(), agent_servers: None, audio: None, auto_update: None, base_keymap: Some(BaseKeymapContent::VSCode), calls: None, collaboration_panel: None, debugger: None, diagnostics: None, disable_ai: None, editor: self.editor_settings_content(), extension: ExtensionSettingsContent::default(), file_finder: None, git: self.git_settings_content(), git_panel: self.git_panel_settings_content(), global_lsp_settings: None, helix_mode: None, image_viewer: None, journal: None, language_models: None, line_indicator_format: None, log: None, message_editor: None, node: self.node_binary_settings(), notification_panel: None, outline_panel: self.outline_panel_settings_content(), preview_tabs: self.preview_tabs_settings_content(), project: self.project_settings_content(), project_panel: self.project_panel_settings_content(), proxy: self.read_string("http.proxy"), remote: RemoteSettingsContent::default(), repl: None, server_url: None, session: None, status_bar: self.status_bar_settings_content(), tab_bar: self.tab_bar_settings_content(), tabs: self.item_settings_content(), telemetry: self.telemetry_settings_content(), terminal: self.terminal_settings_content(), theme: Box::new(self.theme_settings_content()), title_bar: None, vim: None, vim_mode: None, workspace: self.workspace_settings_content(), } } fn agent_settings_content(&self) -> Option<AgentSettingsContent> { let enabled = self.read_bool("chat.agent.enabled"); skip_default(AgentSettingsContent { enabled: enabled, button: enabled, ..Default::default() }) } fn editor_settings_content(&self) -> EditorSettingsContent { EditorSettingsContent { auto_signature_help: self.read_bool("editor.parameterHints.enabled"), autoscroll_on_clicks: None, cursor_blink: self.read_enum("editor.cursorBlinking", |s| match s { "blink" | "phase" | "expand" | "smooth" => Some(true), "solid" => Some(false), _ => None, }), cursor_shape: self.read_enum("editor.cursorStyle", |s| match s { "block" => Some(CursorShape::Block), "block-outline" => Some(CursorShape::Hollow), "line" | "line-thin" => Some(CursorShape::Bar), "underline" | "underline-thin" => Some(CursorShape::Underline), _ => None, }), current_line_highlight: self.read_enum("editor.renderLineHighlight", |s| match s { "gutter" => Some(CurrentLineHighlight::Gutter), "line" => Some(CurrentLineHighlight::Line), "all" => Some(CurrentLineHighlight::All), _ => None, }), diagnostics_max_severity: None, double_click_in_multibuffer: None, drag_and_drop_selection: None, excerpt_context_lines: None, expand_excerpt_lines: None, fast_scroll_sensitivity: self.read_f32("editor.fastScrollSensitivity"), go_to_definition_fallback: None, gutter: self.gutter_content(), hide_mouse: None, horizontal_scroll_margin: None, hover_popover_delay: self.read_u64("editor.hover.delay").map(Into::into), hover_popover_enabled: self.read_bool("editor.hover.enabled"), inline_code_actions: None, jupyter: None, lsp_document_colors: None, lsp_highlight_debounce: None, middle_click_paste: None, minimap: self.minimap_content(), minimum_contrast_for_highlights: None, multi_cursor_modifier: self.read_enum("editor.multiCursorModifier", |s| match s { "ctrlCmd" => Some(MultiCursorModifier::CmdOrCtrl), "alt" => Some(MultiCursorModifier::Alt), _ => None, }), redact_private_values: None, relative_line_numbers: self.read_enum("editor.lineNumbers", |s| match s { "relative" => Some(RelativeLineNumbers::Enabled), _ => None, }), rounded_selection: self.read_bool("editor.roundedSelection"), scroll_beyond_last_line: None, scroll_sensitivity: self.read_f32("editor.mouseWheelScrollSensitivity"), scrollbar: self.scrollbar_content(), search: self.search_content(), search_wrap: None, seed_search_query_from_cursor: self.read_enum( "editor.find.seedSearchStringFromSelection", |s| match s { "always" => Some(SeedQuerySetting::Always), "selection" => Some(SeedQuerySetting::Selection), "never" => Some(SeedQuerySetting::Never), _ => None, }, ), selection_highlight: self.read_bool("editor.selectionHighlight"), show_signature_help_after_edits: self.read_bool("editor.parameterHints.enabled"), snippet_sort_order: None, toolbar: None, use_smartcase_search: self.read_bool("search.smartCase"), vertical_scroll_margin: self.read_f32("editor.cursorSurroundingLines"), completion_menu_scrollbar: None, } } fn gutter_content(&self) -> Option<GutterContent> { skip_default(GutterContent { line_numbers: self.read_enum("editor.lineNumbers", |s| match s { "on" | "relative" => Some(true), "off" => Some(false), _ => None, }), min_line_number_digits: None, runnables: None, breakpoints: None, folds: self.read_enum("editor.showFoldingControls", |s| match s { "always" | "mouseover" => Some(true), "never" => Some(false), _ => None, }), }) } fn scrollbar_content(&self) -> Option<ScrollbarContent> { let scrollbar_axes = skip_default(ScrollbarAxesContent { horizontal: self.read_enum("editor.scrollbar.horizontal", |s| match s { "auto" | "visible" => Some(true), "hidden" => Some(false), _ => None, }), vertical: self.read_enum("editor.scrollbar.vertical", |s| match s { "auto" | "visible" => Some(true), "hidden" => Some(false), _ => None, }), })?; Some(ScrollbarContent { axes: Some(scrollbar_axes), ..Default::default() }) } fn search_content(&self) -> Option<SearchSettingsContent> { skip_default(SearchSettingsContent { include_ignored: self.read_bool("search.useIgnoreFiles"), ..Default::default() }) } fn minimap_content(&self) -> Option<MinimapContent> { let minimap_enabled = self.read_bool("editor.minimap.enabled"); let autohide = self.read_bool("editor.minimap.autohide"); let show = match (minimap_enabled, autohide) { (Some(true), Some(false)) => Some(ShowMinimap::Always), (Some(true), _) => Some(ShowMinimap::Auto), (Some(false), _) => Some(ShowMinimap::Never), _ => None, }; skip_default(MinimapContent { show, thumb: self.read_enum("editor.minimap.showSlider", |s| match s { "always" => Some(MinimapThumb::Always), "mouseover" => Some(MinimapThumb::Hover), _ => None, }), max_width_columns: self .read_u32("editor.minimap.maxColumn") .and_then(|v| NonZeroU32::new(v)), ..Default::default() }) } fn git_panel_settings_content(&self) -> Option<GitPanelSettingsContent> { skip_default(GitPanelSettingsContent { button: self.read_bool("git.enabled"), fallback_branch_name: self.read_string("git.defaultBranchName"), ..Default::default() }) } fn project_settings_content(&self) -> ProjectSettingsContent { ProjectSettingsContent { all_languages: AllLanguageSettingsContent { features: None, edit_predictions: self.edit_predictions_settings_content(), defaults: self.default_language_settings_content(), languages: Default::default(), file_types: self.file_types(), }, worktree: self.worktree_settings_content(), lsp: Default::default(), terminal: None, dap: Default::default(), context_servers: self.context_servers(), load_direnv: None, slash_commands: None, git_hosting_providers: None, } } fn default_language_settings_content(&self) -> LanguageSettingsContent { LanguageSettingsContent { allow_rewrap: None, always_treat_brackets_as_autoclosed: None, auto_indent: None, auto_indent_on_paste: self.read_bool("editor.formatOnPaste"), code_actions_on_format: None, completions: skip_default(CompletionSettingsContent { words: self.read_bool("editor.suggest.showWords").map(|b| { if b { WordsCompletionMode::Enabled } else { WordsCompletionMode::Disabled } }), ..Default::default() }), debuggers: None, edit_predictions_disabled_in: None, enable_language_server: None, ensure_final_newline_on_save: self.read_bool("files.insertFinalNewline"), extend_comment_on_newline: None, format_on_save: self.read_bool("editor.guides.formatOnSave").map(|b| { if b { FormatOnSave::On } else { FormatOnSave::Off } }), formatter: None, hard_tabs: self.read_bool("editor.insertSpaces").map(|v| !v), indent_guides: skip_default(IndentGuideSettingsContent { enabled: self.read_bool("editor.guides.indentation"), ..Default::default() }), inlay_hints: None, jsx_tag_auto_close: None, language_servers: None, linked_edits: self.read_bool("editor.linkedEditing"), preferred_line_length: self.read_u32("editor.wordWrapColumn"), prettier: None, remove_trailing_whitespace_on_save: self.read_bool("editor.trimAutoWhitespace"), show_completion_documentation: None, show_completions_on_input: self.read_bool("editor.suggestOnTriggerCharacters"), show_edit_predictions: self.read_bool("editor.inlineSuggest.enabled"), show_whitespaces: self.read_enum("editor.renderWhitespace", |s| { Some(match s { "boundary" => ShowWhitespaceSetting::Boundary, "trailing" => ShowWhitespaceSetting::Trailing, "selection" => ShowWhitespaceSetting::Selection, "all" => ShowWhitespaceSetting::All, _ => ShowWhitespaceSetting::None, }) }), show_wrap_guides: None, soft_wrap: self.read_enum("editor.wordWrap", |s| match s { "on" => Some(SoftWrap::EditorWidth), "wordWrapColumn" => Some(SoftWrap::PreferLine), "bounded" => Some(SoftWrap::Bounded), "off" => Some(SoftWrap::None), _ => None, }), tab_size: self .read_u32("editor.tabSize") .and_then(|n| NonZeroU32::new(n)), tasks: None, use_auto_surround: self.read_enum("editor.autoSurround", |s| match s { "languageDefined" | "quotes" | "brackets" => Some(true), "never" => Some(false), _ => None, }), use_autoclose: None, use_on_type_format: self.read_bool("editor.formatOnType"), whitespace_map: None, wrap_guides: self .read_value("editor.rulers") .and_then(|v| v.as_array()) .map(|v| { v.iter() .flat_map(|n| n.as_u64().map(|n| n as usize)) .collect() }), } } fn file_types(&self) -> Option<HashMap<Arc<str>, ExtendingVec<String>>> { // vscodes file association map is inverted from ours, so we flip the mapping before merging let mut associations: HashMap<Arc<str>, ExtendingVec<String>> = HashMap::default(); let map = self.read_value("files.associations")?.as_object()?; for (k, v) in map { let Some(v) = v.as_str() else { continue }; associations.entry(v.into()).or_default().0.push(k.clone()); } skip_default(associations) } fn edit_predictions_settings_content(&self) -> Option<EditPredictionSettingsContent> { let disabled_globs = self