890 lines · 33.9 KB · Rust
RawDownload
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

Lines 1–500 of 890.