diff --git a/Cargo.lock b/Cargo.lock index 5566faf25e..e1a99c19c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13167,7 +13167,12 @@ dependencies = [ "command_palette_hooks", "editor", "feature_flags", + "fs", "gpui", + "log", + "paths", + "schemars", + "serde", "settings", "theme", "ui", diff --git a/crates/agent/src/assistant_configuration/tool_picker.rs b/crates/agent/src/assistant_configuration/tool_picker.rs index 2b105e87a2..db52fa8b57 100644 --- a/crates/agent/src/assistant_configuration/tool_picker.rs +++ b/crates/agent/src/assistant_configuration/tool_picker.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use assistant_settings::{ AgentProfile, AgentProfileContent, AgentProfileId, AssistantSettings, AssistantSettingsContent, - ContextServerPresetContent, VersionedAssistantSettingsContent, + ContextServerPresetContent, }; use assistant_tool::{ToolSource, ToolWorkingSet}; use fs::Fs; @@ -201,10 +201,10 @@ impl PickerDelegate for ToolPickerDelegate { let profile_id = self.profile_id.clone(); let default_profile = self.profile.clone(); let tool = tool.clone(); - move |settings, _cx| match settings { - AssistantSettingsContent::Versioned(boxed) => { - if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed { - let profiles = settings.profiles.get_or_insert_default(); + move |settings: &mut AssistantSettingsContent, _cx| { + settings + .v2_setting(|v2_settings| { + let profiles = v2_settings.profiles.get_or_insert_default(); let profile = profiles .entry(profile_id) @@ -240,9 +240,10 @@ impl PickerDelegate for ToolPickerDelegate { *preset.tools.entry(tool.name.clone()).or_default() = is_enabled; } } - } - } - _ => {} + + Ok(()) + }) + .ok(); } }); } diff --git a/crates/assistant/src/slash_command_settings.rs b/crates/assistant/src/slash_command_settings.rs index 25d575ed53..f254d00ec6 100644 --- a/crates/assistant/src/slash_command_settings.rs +++ b/crates/assistant/src/slash_command_settings.rs @@ -44,4 +44,6 @@ impl Settings for SlashCommandSettings { .chain(sources.server), ) } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } diff --git a/crates/assistant_settings/src/assistant_settings.rs b/crates/assistant_settings/src/assistant_settings.rs index 283c1e569d..ec15bdf6f1 100644 --- a/crates/assistant_settings/src/assistant_settings.rs +++ b/crates/assistant_settings/src/assistant_settings.rs @@ -112,13 +112,27 @@ impl AssistantSettings { } /// Assistant panel settings +#[derive(Clone, Serialize, Deserialize, Debug, Default)] +pub struct AssistantSettingsContent { + #[serde(flatten)] + pub inner: Option, +} + #[derive(Clone, Serialize, Deserialize, Debug)] #[serde(untagged)] -pub enum AssistantSettingsContent { +pub enum AssistantSettingsContentInner { Versioned(Box), Legacy(LegacyAssistantSettingsContent), } +impl AssistantSettingsContentInner { + fn for_v2(content: AssistantSettingsContentV2) -> Self { + AssistantSettingsContentInner::Versioned(Box::new(VersionedAssistantSettingsContent::V2( + content, + ))) + } +} + impl JsonSchema for AssistantSettingsContent { fn schema_name() -> String { VersionedAssistantSettingsContent::schema_name() @@ -133,26 +147,21 @@ impl JsonSchema for AssistantSettingsContent { } } -impl Default for AssistantSettingsContent { - fn default() -> Self { - Self::Versioned(Box::new(VersionedAssistantSettingsContent::default())) - } -} - impl AssistantSettingsContent { pub fn is_version_outdated(&self) -> bool { - match self { - AssistantSettingsContent::Versioned(settings) => match **settings { + match &self.inner { + Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings { VersionedAssistantSettingsContent::V1(_) => true, VersionedAssistantSettingsContent::V2(_) => false, }, - AssistantSettingsContent::Legacy(_) => true, + Some(AssistantSettingsContentInner::Legacy(_)) => true, + None => false, } } fn upgrade(&self) -> AssistantSettingsContentV2 { - match self { - AssistantSettingsContent::Versioned(settings) => match **settings { + match &self.inner { + Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings { VersionedAssistantSettingsContent::V1(ref settings) => AssistantSettingsContentV2 { enabled: settings.enabled, button: settings.button, @@ -212,7 +221,7 @@ impl AssistantSettingsContent { }, VersionedAssistantSettingsContent::V2(ref settings) => settings.clone(), }, - AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 { + Some(AssistantSettingsContentInner::Legacy(settings)) => AssistantSettingsContentV2 { enabled: None, button: settings.button, dock: settings.dock, @@ -237,12 +246,13 @@ impl AssistantSettingsContent { always_allow_tool_actions: None, notify_when_agent_waiting: None, }, + None => AssistantSettingsContentV2::default(), } } pub fn set_dock(&mut self, dock: AssistantDockPosition) { - match self { - AssistantSettingsContent::Versioned(settings) => match **settings { + match &mut self.inner { + Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings { VersionedAssistantSettingsContent::V1(ref mut settings) => { settings.dock = Some(dock); } @@ -250,9 +260,17 @@ impl AssistantSettingsContent { settings.dock = Some(dock); } }, - AssistantSettingsContent::Legacy(settings) => { + Some(AssistantSettingsContentInner::Legacy(settings)) => { settings.dock = Some(dock); } + None => { + self.inner = Some(AssistantSettingsContentInner::for_v2( + AssistantSettingsContentV2 { + dock: Some(dock), + ..Default::default() + }, + )) + } } } @@ -260,8 +278,8 @@ impl AssistantSettingsContent { let model = language_model.id().0.to_string(); let provider = language_model.provider_id().0.to_string(); - match self { - AssistantSettingsContent::Versioned(settings) => match **settings { + match &mut self.inner { + Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings { VersionedAssistantSettingsContent::V1(ref mut settings) => { match provider.as_ref() { "zed.dev" => { @@ -337,56 +355,80 @@ impl AssistantSettingsContent { settings.default_model = Some(LanguageModelSelection { provider, model }); } }, - AssistantSettingsContent::Legacy(settings) => { + Some(AssistantSettingsContentInner::Legacy(settings)) => { if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) { settings.default_open_ai_model = Some(model); } } + None => { + self.inner = Some(AssistantSettingsContentInner::for_v2( + AssistantSettingsContentV2 { + default_model: Some(LanguageModelSelection { provider, model }), + ..Default::default() + }, + )); + } } } pub fn set_inline_assistant_model(&mut self, provider: String, model: String) { - if let AssistantSettingsContent::Versioned(boxed) = self { - if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed { - settings.inline_assistant_model = Some(LanguageModelSelection { provider, model }); - } - } + self.v2_setting(|setting| { + setting.inline_assistant_model = Some(LanguageModelSelection { provider, model }); + Ok(()) + }) + .ok(); } pub fn set_commit_message_model(&mut self, provider: String, model: String) { - if let AssistantSettingsContent::Versioned(boxed) = self { - if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed { - settings.commit_message_model = Some(LanguageModelSelection { provider, model }); + self.v2_setting(|setting| { + setting.commit_message_model = Some(LanguageModelSelection { provider, model }); + Ok(()) + }) + .ok(); + } + + pub fn v2_setting( + &mut self, + f: impl FnOnce(&mut AssistantSettingsContentV2) -> anyhow::Result<()>, + ) -> anyhow::Result<()> { + match self.inner.get_or_insert_with(|| { + AssistantSettingsContentInner::for_v2(AssistantSettingsContentV2 { + ..Default::default() + }) + }) { + AssistantSettingsContentInner::Versioned(boxed) => { + if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed { + f(settings) + } else { + Ok(()) + } } + _ => Ok(()), } } pub fn set_thread_summary_model(&mut self, provider: String, model: String) { - if let AssistantSettingsContent::Versioned(boxed) = self { - if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed { - settings.thread_summary_model = Some(LanguageModelSelection { provider, model }); - } - } + self.v2_setting(|setting| { + setting.thread_summary_model = Some(LanguageModelSelection { provider, model }); + Ok(()) + }) + .ok(); } pub fn set_always_allow_tool_actions(&mut self, allow: bool) { - let AssistantSettingsContent::Versioned(boxed) = self else { - return; - }; - - if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed { - settings.always_allow_tool_actions = Some(allow); - } + self.v2_setting(|setting| { + setting.always_allow_tool_actions = Some(allow); + Ok(()) + }) + .ok(); } pub fn set_profile(&mut self, profile_id: AgentProfileId) { - let AssistantSettingsContent::Versioned(boxed) = self else { - return; - }; - - if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed { - settings.default_profile = Some(profile_id); - } + self.v2_setting(|setting| { + setting.default_profile = Some(profile_id); + Ok(()) + }) + .ok(); } pub fn create_profile( @@ -394,11 +436,7 @@ impl AssistantSettingsContent { profile_id: AgentProfileId, profile: AgentProfile, ) -> Result<()> { - let AssistantSettingsContent::Versioned(boxed) = self else { - return Ok(()); - }; - - if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed { + self.v2_setting(|settings| { let profiles = settings.profiles.get_or_insert_default(); if profiles.contains_key(&profile_id) { bail!("profile with ID '{profile_id}' already exists"); @@ -424,9 +462,9 @@ impl AssistantSettingsContent { .collect(), }, ); - } - Ok(()) + Ok(()) + }) } } @@ -461,7 +499,7 @@ impl Default for VersionedAssistantSettingsContent { } } -#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)] +#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)] pub struct AssistantSettingsContentV2 { /// Whether the Assistant is enabled. /// @@ -708,6 +746,39 @@ impl Settings for AssistantSettings { Ok(settings) } + + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + if let Some(b) = vscode + .read_value("chat.agent.enabled") + .and_then(|b| b.as_bool()) + { + match &mut current.inner { + Some(AssistantSettingsContentInner::Versioned(versioned)) => { + match versioned.as_mut() { + VersionedAssistantSettingsContent::V1(setting) => { + setting.enabled = Some(b); + setting.button = Some(b); + } + + VersionedAssistantSettingsContent::V2(setting) => { + setting.enabled = Some(b); + setting.button = Some(b); + } + } + } + Some(AssistantSettingsContentInner::Legacy(setting)) => setting.button = Some(b), + None => { + current.inner = Some(AssistantSettingsContentInner::for_v2( + AssistantSettingsContentV2 { + enabled: Some(b), + button: Some(b), + ..Default::default() + }, + )); + } + } + } + } } fn merge(target: &mut T, value: Option) { @@ -751,28 +822,30 @@ mod tests { settings::SettingsStore::global(cx).update_settings_file::( fs.clone(), |settings, _| { - *settings = AssistantSettingsContent::Versioned(Box::new( - VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 { - default_model: Some(LanguageModelSelection { - provider: "test-provider".into(), - model: "gpt-99".into(), - }), - inline_assistant_model: None, - commit_message_model: None, - thread_summary_model: None, - inline_alternatives: None, - enabled: None, - button: None, - dock: None, - default_width: None, - default_height: None, - enable_experimental_live_diffs: None, - default_profile: None, - profiles: None, - always_allow_tool_actions: None, - notify_when_agent_waiting: None, - }), - )) + *settings = AssistantSettingsContent { + inner: Some(AssistantSettingsContentInner::for_v2( + AssistantSettingsContentV2 { + default_model: Some(LanguageModelSelection { + provider: "test-provider".into(), + model: "gpt-99".into(), + }), + inline_assistant_model: None, + commit_message_model: None, + thread_summary_model: None, + inline_alternatives: None, + enabled: None, + button: None, + dock: None, + default_width: None, + default_height: None, + enable_experimental_live_diffs: None, + default_profile: None, + profiles: None, + always_allow_tool_actions: None, + notify_when_agent_waiting: None, + }, + )), + } }, ); }); diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 390400c048..3878bab981 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -118,6 +118,13 @@ impl Settings for AutoUpdateSetting { Ok(Self(auto_update.0)) } + + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + vscode.enum_setting("update.mode", current, |s| match s { + "none" | "manual" => Some(AutoUpdateSettingContent(false)), + _ => Some(AutoUpdateSettingContent(true)), + }); + } } #[derive(Default)] diff --git a/crates/call/src/call_settings.rs b/crates/call/src/call_settings.rs index e15fd38067..c8f51e0c1a 100644 --- a/crates/call/src/call_settings.rs +++ b/crates/call/src/call_settings.rs @@ -32,4 +32,6 @@ impl Settings for CallSettings { fn load(sources: SettingsSources, _: &mut App) -> Result { sources.json_merge() } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index afb4ca06ea..db832ac134 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -104,6 +104,8 @@ impl Settings for ClientSettings { } Ok(result) } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } #[derive(Default, Clone, Serialize, Deserialize, JsonSchema)] @@ -130,6 +132,10 @@ impl Settings for ProxySettings { .or(sources.default.proxy.clone()), }) } + + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + vscode.string_setting("http.proxy", &mut current.proxy); + } } pub fn init_settings(cx: &mut App) { @@ -518,6 +524,18 @@ impl settings::Settings for TelemetrySettings { .unwrap_or(sources.default.metrics.ok_or_else(Self::missing_default)?), }) } + + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + vscode.enum_setting("telemetry.telemetryLevel", &mut current.metrics, |s| { + Some(s == "all") + }); + vscode.enum_setting("telemetry.telemetryLevel", &mut current.diagnostics, |s| { + Some(matches!(s, "all" | "error" | "crash")) + }); + // we could translate telemetry.telemetryLevel, but just because users didn't want + // to send microsoft telemetry doesn't mean they don't want to send it to zed. their + // all/error/crash/off correspond to combinations of our "diagnostics" and "metrics". + } } impl Client { diff --git a/crates/collab_ui/src/panel_settings.rs b/crates/collab_ui/src/panel_settings.rs index f5fd3531b1..652d9eb67f 100644 --- a/crates/collab_ui/src/panel_settings.rs +++ b/crates/collab_ui/src/panel_settings.rs @@ -86,6 +86,8 @@ impl Settings for CollaborationPanelSettings { ) -> anyhow::Result { sources.json_merge() } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } impl Settings for ChatPanelSettings { @@ -99,6 +101,8 @@ impl Settings for ChatPanelSettings { ) -> anyhow::Result { sources.json_merge() } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } impl Settings for NotificationPanelSettings { @@ -112,6 +116,8 @@ impl Settings for NotificationPanelSettings { ) -> anyhow::Result { sources.json_merge() } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } impl Settings for MessageEditorSettings { @@ -125,4 +131,6 @@ impl Settings for MessageEditorSettings { ) -> anyhow::Result { sources.json_merge() } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } diff --git a/crates/context_server_settings/src/context_server_settings.rs b/crates/context_server_settings/src/context_server_settings.rs index a8cfc12571..8047eab297 100644 --- a/crates/context_server_settings/src/context_server_settings.rs +++ b/crates/context_server_settings/src/context_server_settings.rs @@ -58,4 +58,42 @@ impl Settings for ContextServerSettings { ) -> anyhow::Result { sources.json_merge() } + + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + // we don't handle "inputs" replacement strings, see perplexity-key in this example: + // https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_configuration-example + #[derive(Deserialize)] + struct VsCodeServerCommand { + command: String, + args: Option>, + env: Option>, + // note: we don't support envFile and type + } + impl From for ServerCommand { + fn from(cmd: VsCodeServerCommand) -> Self { + Self { + path: cmd.command, + args: cmd.args.unwrap_or_default(), + env: cmd.env, + } + } + } + if let Some(mcp) = vscode.read_value("mcp").and_then(|v| v.as_object()) { + current + .context_servers + .extend(mcp.iter().filter_map(|(k, v)| { + Some(( + k.clone().into(), + ServerConfig { + command: Some( + serde_json::from_value::(v.clone()) + .ok()? + .into(), + ), + settings: None, + }, + )) + })); + } + } } diff --git a/crates/dap/src/debugger_settings.rs b/crates/dap/src/debugger_settings.rs index 5b83927dfe..be89deea0d 100644 --- a/crates/dap/src/debugger_settings.rs +++ b/crates/dap/src/debugger_settings.rs @@ -54,6 +54,8 @@ impl Settings for DebuggerSettings { fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { sources.json_merge() } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } impl Global for DebuggerSettings {} diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 25604462c3..04e477a5ea 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -2,7 +2,7 @@ use gpui::App; use language::CursorShape; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, VsCodeSettings}; #[derive(Deserialize, Clone)] pub struct EditorSettings { @@ -388,7 +388,7 @@ pub struct ToolbarContent { } /// Scrollbar related settings -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)] pub struct ScrollbarContent { /// When to show the scrollbar in the editor. /// @@ -423,7 +423,7 @@ pub struct ScrollbarContent { } /// Forcefully enable or disable the scrollbar for each axis -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)] pub struct ScrollbarAxesContent { /// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings. /// @@ -475,4 +475,164 @@ impl Settings for EditorSettings { fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { sources.json_merge() } + + fn import_from_vscode(vscode: &VsCodeSettings, current: &mut Self::FileContent) { + vscode.enum_setting( + "editor.cursorBlinking", + &mut current.cursor_blink, + |s| match s { + "blink" | "phase" | "expand" | "smooth" => Some(true), + "solid" => Some(false), + _ => None, + }, + ); + vscode.enum_setting( + "editor.cursorStyle", + &mut current.cursor_shape, + |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, + }, + ); + + vscode.enum_setting( + "editor.renderLineHighlight", + &mut current.current_line_highlight, + |s| match s { + "gutter" => Some(CurrentLineHighlight::Gutter), + "line" => Some(CurrentLineHighlight::Line), + "all" => Some(CurrentLineHighlight::All), + _ => None, + }, + ); + + vscode.bool_setting( + "editor.selectionHighlight", + &mut current.selection_highlight, + ); + vscode.bool_setting("editor.hover.enabled", &mut current.hover_popover_enabled); + vscode.u64_setting("editor.hover.delay", &mut current.hover_popover_delay); + + let mut gutter = GutterContent::default(); + vscode.enum_setting( + "editor.showFoldingControls", + &mut gutter.folds, + |s| match s { + "always" | "mouseover" => Some(true), + "never" => Some(false), + _ => None, + }, + ); + vscode.enum_setting( + "editor.lineNumbers", + &mut gutter.line_numbers, + |s| match s { + "on" | "relative" => Some(true), + "off" => Some(false), + _ => None, + }, + ); + if let Some(old_gutter) = current.gutter.as_mut() { + if gutter.folds.is_some() { + old_gutter.folds = gutter.folds + } + if gutter.line_numbers.is_some() { + old_gutter.line_numbers = gutter.line_numbers + } + } else { + if gutter != GutterContent::default() { + current.gutter = Some(gutter) + } + } + if let Some(b) = vscode.read_bool("editor.scrollBeyondLastLine") { + current.scroll_beyond_last_line = Some(if b { + ScrollBeyondLastLine::OnePage + } else { + ScrollBeyondLastLine::Off + }) + } + + let mut scrollbar_axes = ScrollbarAxesContent::default(); + vscode.enum_setting( + "editor.scrollbar.horizontal", + &mut scrollbar_axes.horizontal, + |s| match s { + "auto" | "visible" => Some(true), + "hidden" => Some(false), + _ => None, + }, + ); + vscode.enum_setting( + "editor.scrollbar.vertical", + &mut scrollbar_axes.horizontal, + |s| match s { + "auto" | "visible" => Some(true), + "hidden" => Some(false), + _ => None, + }, + ); + + if scrollbar_axes != ScrollbarAxesContent::default() { + let scrollbar_settings = current.scrollbar.get_or_insert_default(); + let axes_settings = scrollbar_settings.axes.get_or_insert_default(); + + if let Some(vertical) = scrollbar_axes.vertical { + axes_settings.vertical = Some(vertical); + } + if let Some(horizontal) = scrollbar_axes.horizontal { + axes_settings.horizontal = Some(horizontal); + } + } + + // TODO: check if this does the int->float conversion? + vscode.f32_setting( + "editor.cursorSurroundingLines", + &mut current.vertical_scroll_margin, + ); + vscode.f32_setting( + "editor.mouseWheelScrollSensitivity", + &mut current.scroll_sensitivity, + ); + if Some("relative") == vscode.read_string("editor.lineNumbers") { + current.relative_line_numbers = Some(true); + } + + vscode.enum_setting( + "editor.find.seedSearchStringFromSelection", + &mut current.seed_search_query_from_cursor, + |s| match s { + "always" => Some(SeedQuerySetting::Always), + "selection" => Some(SeedQuerySetting::Selection), + "never" => Some(SeedQuerySetting::Never), + _ => None, + }, + ); + vscode.bool_setting("search.smartCase", &mut current.use_smartcase_search); + vscode.enum_setting( + "editor.multiCursorModifier", + &mut current.multi_cursor_modifier, + |s| match s { + "ctrlCmd" => Some(MultiCursorModifier::CmdOrCtrl), + "alt" => Some(MultiCursorModifier::Alt), + _ => None, + }, + ); + + vscode.bool_setting( + "editor.parameterHints.enabled", + &mut current.auto_signature_help, + ); + vscode.bool_setting( + "editor.parameterHints.enabled", + &mut current.show_signature_help_after_edits, + ); + + if let Some(use_ignored) = vscode.read_bool("search.useIgnoreFiles") { + let search = current.search.get_or_insert_default(); + search.include_ignored = use_ignored; + } + } } diff --git a/crates/extension_host/src/extension_settings.rs b/crates/extension_host/src/extension_settings.rs index 3689624019..cfa67990b0 100644 --- a/crates/extension_host/src/extension_settings.rs +++ b/crates/extension_host/src/extension_settings.rs @@ -50,4 +50,10 @@ impl Settings for ExtensionSettings { .chain(sources.server), ) } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) { + // settingsSync.ignoredExtensions controls autoupdate for vscode extensions, but we + // don't have a mapping to zed-extensions. there's also extensions.autoCheckUpdates + // and extensions.autoUpdate which are global switches, we don't support those yet + } } diff --git a/crates/file_finder/src/file_finder_settings.rs b/crates/file_finder/src/file_finder_settings.rs index 337eab3902..0a03287ba7 100644 --- a/crates/file_finder/src/file_finder_settings.rs +++ b/crates/file_finder/src/file_finder_settings.rs @@ -29,6 +29,8 @@ impl Settings for FileFinderSettings { fn load(sources: SettingsSources, _: &mut gpui::App) -> Result { sources.json_merge() } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } #[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)] diff --git a/crates/git_hosting_providers/src/settings.rs b/crates/git_hosting_providers/src/settings.rs index 0901ae3141..4273492e2d 100644 --- a/crates/git_hosting_providers/src/settings.rs +++ b/crates/git_hosting_providers/src/settings.rs @@ -81,4 +81,6 @@ impl Settings for GitHostingProviderSettings { fn load(sources: settings::SettingsSources, _: &mut App) -> Result { sources.json_merge() } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } diff --git a/crates/git_ui/src/git_panel_settings.rs b/crates/git_ui/src/git_panel_settings.rs index 99710225fc..198c4815c7 100644 --- a/crates/git_ui/src/git_panel_settings.rs +++ b/crates/git_ui/src/git_panel_settings.rs @@ -87,4 +87,9 @@ impl Settings for GitPanelSettings { ) -> anyhow::Result { sources.json_merge() } + + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + vscode.bool_setting("git.enabled", &mut current.button); + vscode.string_setting("git.defaultBranchName", &mut current.fallback_branch_name); + } } diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index 9173be660b..95e53a0978 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -280,10 +280,6 @@ pub(crate) enum LineIndicatorFormat { Long, } -/// Whether or not to automatically check for updates. -/// -/// Values: short, long -/// Default: short #[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize)] #[serde(transparent)] pub(crate) struct LineIndicatorFormatContent(LineIndicatorFormat); @@ -301,4 +297,6 @@ impl Settings for LineIndicatorFormat { Ok(format.0) } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } diff --git a/crates/image_viewer/src/image_viewer_settings.rs b/crates/image_viewer/src/image_viewer_settings.rs index 165e3c4a44..a9989eb5ad 100644 --- a/crates/image_viewer/src/image_viewer_settings.rs +++ b/crates/image_viewer/src/image_viewer_settings.rs @@ -39,4 +39,6 @@ impl Settings for ImageViewerSettings { .chain(sources.server), ) } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index b75ae93825..0aed317a0b 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -53,6 +53,8 @@ impl settings::Settings for JournalSettings { fn load(sources: SettingsSources, _: &mut App) -> Result { sources.json_merge() } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } pub fn init(_: Arc, cx: &mut App) { diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 56ffbbef2f..96610846bd 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -1219,11 +1219,11 @@ impl settings::Settings for AllLanguageSettings { let mut file_types: FxHashMap, GlobSet> = FxHashMap::default(); - for (language, suffixes) in &default_value.file_types { + for (language, patterns) in &default_value.file_types { let mut builder = GlobSetBuilder::new(); - for suffix in suffixes { - builder.add(Glob::new(suffix)?); + for pattern in patterns { + builder.add(Glob::new(pattern)?); } file_types.insert(language.clone(), builder.build()?); @@ -1280,20 +1280,20 @@ impl settings::Settings for AllLanguageSettings { ); } - for (language, suffixes) in &user_settings.file_types { + for (language, patterns) in &user_settings.file_types { let mut builder = GlobSetBuilder::new(); let default_value = default_value.file_types.get(&language.clone()); // Merge the default value with the user's value. - if let Some(suffixes) = default_value { - for suffix in suffixes { - builder.add(Glob::new(suffix)?); + if let Some(patterns) = default_value { + for pattern in patterns { + builder.add(Glob::new(pattern)?); } } - for suffix in suffixes { - builder.add(Glob::new(suffix)?); + for pattern in patterns { + builder.add(Glob::new(pattern)?); } file_types.insert(language.clone(), builder.build()?); @@ -1370,6 +1370,120 @@ impl settings::Settings for AllLanguageSettings { root_schema } + + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + let d = &mut current.defaults; + if let Some(size) = vscode + .read_value("editor.tabSize") + .and_then(|v| v.as_u64()) + .and_then(|n| NonZeroU32::new(n as u32)) + { + d.tab_size = Some(size); + } + if let Some(v) = vscode.read_bool("editor.insertSpaces") { + d.hard_tabs = Some(!v); + } + + vscode.enum_setting("editor.wordWrap", &mut d.soft_wrap, |s| match s { + "on" => Some(SoftWrap::EditorWidth), + "wordWrapColumn" => Some(SoftWrap::PreferLine), + "bounded" => Some(SoftWrap::Bounded), + "off" => Some(SoftWrap::None), + _ => None, + }); + vscode.u32_setting("editor.wordWrapColumn", &mut d.preferred_line_length); + + if let Some(arr) = vscode + .read_value("editor.rulers") + .and_then(|v| v.as_array()) + .map(|v| v.iter().map(|n| n.as_u64().map(|n| n as usize)).collect()) + { + d.wrap_guides = arr; + } + if let Some(b) = vscode.read_bool("editor.guides.indentation") { + if let Some(guide_settings) = d.indent_guides.as_mut() { + guide_settings.enabled = b; + } else { + d.indent_guides = Some(IndentGuideSettings { + enabled: b, + ..Default::default() + }); + } + } + + if let Some(b) = vscode.read_bool("editor.guides.formatOnSave") { + d.format_on_save = Some(if b { + FormatOnSave::On + } else { + FormatOnSave::Off + }); + } + vscode.bool_setting( + "editor.trimAutoWhitespace", + &mut d.remove_trailing_whitespace_on_save, + ); + vscode.bool_setting( + "files.insertFinalNewline", + &mut d.ensure_final_newline_on_save, + ); + vscode.bool_setting("editor.inlineSuggest.enabled", &mut d.show_edit_predictions); + vscode.enum_setting("editor.renderWhitespace", &mut d.show_whitespaces, |s| { + Some(match s { + "boundary" | "trailing" => ShowWhitespaceSetting::Boundary, + "selection" => ShowWhitespaceSetting::Selection, + "all" => ShowWhitespaceSetting::All, + _ => ShowWhitespaceSetting::None, + }) + }); + vscode.enum_setting( + "editor.autoSurround", + &mut d.use_auto_surround, + |s| match s { + "languageDefined" | "quotes" | "brackets" => Some(true), + "never" => Some(false), + _ => None, + }, + ); + vscode.bool_setting("editor.formatOnType", &mut d.use_on_type_format); + vscode.bool_setting("editor.linkedEditing", &mut d.linked_edits); + vscode.bool_setting("editor.formatOnPaste", &mut d.auto_indent_on_paste); + vscode.bool_setting( + "editor.suggestOnTriggerCharacters", + &mut d.show_completions_on_input, + ); + if let Some(b) = vscode.read_bool("editor.suggest.showWords") { + let mode = if b { + WordsCompletionMode::Enabled + } else { + WordsCompletionMode::Disabled + }; + if let Some(completion_settings) = d.completions.as_mut() { + completion_settings.words = mode; + } else { + d.completions = Some(CompletionSettings { + words: mode, + lsp: true, + lsp_fetch_timeout_ms: 0, + lsp_insert_mode: LspInsertMode::ReplaceSuffix, + }); + } + } + // TODO: pull ^ out into helper and reuse for per-language settings + + // vscodes file association map is inverted from ours, so we flip the mapping before merging + let mut associations: HashMap, Vec> = HashMap::default(); + if let Some(map) = vscode + .read_value("files.associations") + .and_then(|v| v.as_object()) + { + for (k, v) in map { + let Some(v) = v.as_str() else { continue }; + associations.entry(v.into()).or_default().push(k.clone()); + } + } + // TODO: do we want to merge imported globs per filetype? for now we'll just replace + current.file_types.extend(associations); + } } fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent) { diff --git a/crates/language_models/src/settings.rs b/crates/language_models/src/settings.rs index 9ac058f3c9..899d325689 100644 --- a/crates/language_models/src/settings.rs +++ b/crates/language_models/src/settings.rs @@ -413,4 +413,6 @@ impl settings::Settings for AllLanguageModelSettings { Ok(settings) } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } diff --git a/crates/outline_panel/src/outline_panel_settings.rs b/crates/outline_panel/src/outline_panel_settings.rs index 0eac016239..6b70cb54fb 100644 --- a/crates/outline_panel/src/outline_panel_settings.rs +++ b/crates/outline_panel/src/outline_panel_settings.rs @@ -118,4 +118,13 @@ impl Settings for OutlinePanelSettings { ) -> anyhow::Result { sources.json_merge() } + + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + if let Some(b) = vscode.read_bool("outline.icons") { + current.file_icons = Some(b); + current.folder_icons = Some(b); + } + + vscode.bool_setting("git.decorations.enabled", &mut current.git_status); + } } diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index b0e5f509c3..d10fefbf6a 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -410,3 +410,18 @@ pub fn local_debug_file_relative_path() -> &'static Path { pub fn local_vscode_launch_file_relative_path() -> &'static Path { Path::new(".vscode/launch.json") } + +/// Returns the path to the vscode user settings file +pub fn vscode_settings_file() -> &'static PathBuf { + static LOGS_DIR: OnceLock = OnceLock::new(); + let rel_path = "Code/User/Settings.json"; + LOGS_DIR.get_or_init(|| { + if cfg!(target_os = "macos") { + home_dir() + .join("Library/Application Support") + .join(rel_path) + } else { + config_dir().join(rel_path) + } + }) +} diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index b9cb13a948..7f95a53eb5 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -84,7 +84,7 @@ pub struct NodeBinarySettings { pub path: Option, /// The path to the npm binary Zed should use (defaults to `.path/../npm`). pub npm_path: Option, - /// If disabled, Zed will download its own copy of Node. + /// If enabled, Zed will download its own copy of Node. #[serde(default)] pub ignore_system_version: Option, } @@ -330,6 +330,32 @@ impl Settings for ProjectSettings { fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { sources.json_merge() } + + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + // this just sets the binary name instead of a full path so it relies on path lookup + // resolving to the one you want + vscode.enum_setting( + "npm.packageManager", + &mut current.node.npm_path, + |s| match s { + v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()), + _ => None, + }, + ); + + if let Some(b) = vscode.read_bool("git.blame.editorDecoration.enabled") { + if let Some(blame) = current.git.inline_blame.as_mut() { + blame.enabled = b + } else { + current.git.inline_blame = Some(InlineBlameSettings { + enabled: b, + ..Default::default() + }) + } + } + + // TODO: translate lsp settings for rust-analyzer and other popular ones to old.lsp + } } pub enum SettingsObserverMode { diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index b59c3611be..54b4a4840a 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -158,4 +158,24 @@ impl Settings for ProjectPanelSettings { ) -> anyhow::Result { sources.json_merge() } + + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + vscode.bool_setting("explorer.excludeGitIgnore", &mut current.hide_gitignore); + vscode.bool_setting("explorer.autoReveal", &mut current.auto_reveal_entries); + vscode.bool_setting("explorer.compactFolders", &mut current.auto_fold_dirs); + + if Some(false) == vscode.read_bool("git.decorations.enabled") { + current.git_status = Some(false); + } + if Some(false) == vscode.read_bool("problems.decorations.enabled") { + current.show_diagnostics = Some(ShowDiagnostics::Off); + } + if let (Some(false), Some(false)) = ( + vscode.read_bool("explorer.decorations.badges"), + vscode.read_bool("explorer.decorations.colors"), + ) { + current.git_status = Some(false); + current.show_diagnostics = Some(ShowDiagnostics::Off); + } + } } diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index f37551d561..95e0c3732c 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -125,6 +125,8 @@ impl Settings for SshSettings { fn load(sources: SettingsSources, _: &mut App) -> Result { sources.json_merge() } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } pub struct SshPrompt { diff --git a/crates/repl/src/jupyter_settings.rs b/crates/repl/src/jupyter_settings.rs index 01c0ecbaea..8b00e0f757 100644 --- a/crates/repl/src/jupyter_settings.rs +++ b/crates/repl/src/jupyter_settings.rs @@ -60,4 +60,6 @@ impl Settings for JupyterSettings { Ok(settings) } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 5f695cb62b..1e234327db 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -4,6 +4,7 @@ mod key_equivalents; mod keymap_file; mod settings_file; mod settings_store; +mod vscode_import; use gpui::App; use rust_embed::RustEmbed; @@ -21,6 +22,7 @@ pub use settings_store::{ InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources, SettingsStore, TaskKind, parse_json_with_comments, }; +pub use vscode_import::VsCodeSettings; #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] pub struct WorktreeId(usize); diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index de696c69d0..d333d12951 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -10,6 +10,7 @@ use paths::{ }; use schemars::{JsonSchema, r#gen::SchemaGenerator, schema::RootSchema}; use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use serde_json::Value; use smallvec::SmallVec; use std::{ any::{Any, TypeId, type_name}, @@ -27,7 +28,7 @@ use util::{ResultExt as _, merge_non_null_json_value_into}; pub type EditorconfigProperties = ec4rs::Properties; -use crate::{SettingsJsonSchemaParams, WorktreeId}; +use crate::{SettingsJsonSchemaParams, VsCodeSettings, WorktreeId}; /// A value that can be defined as a user setting. /// @@ -68,6 +69,10 @@ pub trait Settings: 'static + Send + Sync { anyhow::anyhow!("missing default") } + /// Use [the helpers in the vscode_import module](crate::vscode_import) to apply known + /// equivalent settings from a vscode config to our config + fn import_from_vscode(vscode: &VsCodeSettings, current: &mut Self::FileContent); + #[track_caller] fn register(cx: &mut App) where @@ -149,7 +154,7 @@ impl<'a, T: Serialize> SettingsSources<'a, T> { pub fn json_merge_with( customizations: impl Iterator, ) -> Result { - let mut merged = serde_json::Value::Null; + let mut merged = Value::Null; for value in customizations { merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged); } @@ -174,11 +179,11 @@ pub struct SettingsLocation<'a> { /// A set of strongly-typed setting values defined via multiple config files. pub struct SettingsStore { setting_values: HashMap>, - raw_default_settings: serde_json::Value, - raw_user_settings: serde_json::Value, - raw_server_settings: Option, - raw_extension_settings: serde_json::Value, - raw_local_settings: BTreeMap<(WorktreeId, Arc), serde_json::Value>, + raw_default_settings: Value, + raw_user_settings: Value, + raw_server_settings: Option, + raw_extension_settings: Value, + raw_local_settings: BTreeMap<(WorktreeId, Arc), Value>, raw_editorconfig_settings: BTreeMap<(WorktreeId, Arc), (String, Option)>, tab_size_callback: Option<( TypeId, @@ -233,7 +238,7 @@ struct SettingValue { trait AnySettingValue: 'static + Send + Sync { fn key(&self) -> Option<&'static str>; fn setting_type_name(&self) -> &'static str; - fn deserialize_setting(&self, json: &serde_json::Value) -> Result; + fn deserialize_setting(&self, json: &Value) -> Result; fn load_setting( &self, sources: SettingsSources, @@ -248,6 +253,14 @@ trait AnySettingValue: 'static + Send + Sync { _: &SettingsJsonSchemaParams, cx: &App, ) -> RootSchema; + fn edits_for_update( + &self, + raw_settings: &serde_json::Value, + tab_size: usize, + vscode_settings: &VsCodeSettings, + text: &mut String, + edits: &mut Vec<(Range, String)>, + ); } struct DeserializedSetting(Box); @@ -380,7 +393,7 @@ impl SettingsStore { /// /// For user-facing functionality use the typed setting interface. /// (e.g. ProjectSettings::get_global(cx)) - pub fn raw_user_settings(&self) -> &serde_json::Value { + pub fn raw_user_settings(&self) -> &Value { &self.raw_user_settings } @@ -461,6 +474,41 @@ impl SettingsStore { .ok(); } + pub fn import_vscode_settings(&self, fs: Arc, vscode_settings: VsCodeSettings) { + self.setting_file_updates_tx + .unbounded_send(Box::new(move |cx: AsyncApp| { + async move { + let old_text = Self::load_settings(&fs).await?; + let new_text = cx.read_global(|store: &SettingsStore, _cx| { + store.get_vscode_edits(old_text, &vscode_settings) + })?; + let settings_path = paths::settings_file().as_path(); + if fs.is_file(settings_path).await { + let resolved_path = + fs.canonicalize(settings_path).await.with_context(|| { + format!("Failed to canonicalize settings path {:?}", settings_path) + })?; + + fs.atomic_write(resolved_path.clone(), new_text) + .await + .with_context(|| { + format!("Failed to write settings to file {:?}", resolved_path) + })?; + } else { + fs.atomic_write(settings_path.to_path_buf(), new_text) + .await + .with_context(|| { + format!("Failed to write settings to file {:?}", settings_path) + })?; + } + + anyhow::Ok(()) + } + .boxed_local() + })) + .ok(); + } + /// Updates the value of a setting in a JSON file, returning the new text /// for that JSON file. pub fn new_text_for_update( @@ -476,6 +524,20 @@ impl SettingsStore { new_text } + pub fn get_vscode_edits(&self, mut old_text: String, vscode: &VsCodeSettings) -> String { + let mut new_text = old_text.clone(); + let mut edits: Vec<(Range, String)> = Vec::new(); + let raw_settings = parse_json_with_comments::(&old_text).unwrap_or_default(); + let tab_size = self.json_tab_size(); + for v in self.setting_values.values() { + v.edits_for_update(&raw_settings, tab_size, vscode, &mut old_text, &mut edits); + } + for (range, replacement) in edits.into_iter() { + new_text.replace_range(range, &replacement); + } + new_text + } + /// Updates the value of a setting in a JSON file, returning a list /// of edits to apply to the JSON file. pub fn edits_for_update( @@ -491,7 +553,7 @@ impl SettingsStore { .setting_values .get(&setting_type_id) .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())); - let raw_settings = parse_json_with_comments::(text).unwrap_or_default(); + let raw_settings = parse_json_with_comments::(text).unwrap_or_default(); let old_content = match setting.deserialize_setting(&raw_settings) { Ok(content) => content.0.downcast::().unwrap(), Err(_) => Box::<::FileContent>::default(), @@ -555,7 +617,7 @@ impl SettingsStore { default_settings_content: &str, cx: &mut App, ) -> Result<()> { - let settings: serde_json::Value = parse_json_with_comments(default_settings_content)?; + let settings: Value = parse_json_with_comments(default_settings_content)?; if settings.is_object() { self.raw_default_settings = settings; self.recompute_values(None, cx)?; @@ -570,8 +632,8 @@ impl SettingsStore { &mut self, user_settings_content: &str, cx: &mut App, - ) -> Result { - let settings: serde_json::Value = if user_settings_content.is_empty() { + ) -> Result { + let settings: Value = if user_settings_content.is_empty() { parse_json_with_comments("{}")? } else { parse_json_with_comments(user_settings_content)? @@ -588,7 +650,7 @@ impl SettingsStore { server_settings_content: &str, cx: &mut App, ) -> Result<()> { - let settings: Option = if server_settings_content.is_empty() { + let settings: Option = if server_settings_content.is_empty() { None } else { parse_json_with_comments(server_settings_content)? @@ -639,10 +701,12 @@ impl SettingsStore { .remove(&(root_id, directory_path.clone())); } (LocalSettingsKind::Settings, Some(settings_contents)) => { - let new_settings = parse_json_with_comments::(settings_contents) - .map_err(|e| InvalidSettingsError::LocalSettings { - path: directory_path.join(local_settings_file_relative_path()), - message: e.to_string(), + let new_settings = + parse_json_with_comments::(settings_contents).map_err(|e| { + InvalidSettingsError::LocalSettings { + path: directory_path.join(local_settings_file_relative_path()), + message: e.to_string(), + } })?; match self .raw_local_settings @@ -707,7 +771,7 @@ impl SettingsStore { } pub fn set_extension_settings(&mut self, content: T, cx: &mut App) -> Result<()> { - let settings: serde_json::Value = serde_json::to_value(content)?; + let settings: Value = serde_json::to_value(content)?; anyhow::ensure!(settings.is_object(), "settings must be an object"); self.raw_extension_settings = settings; self.recompute_values(None, cx)?; @@ -754,11 +818,7 @@ impl SettingsStore { }) } - pub fn json_schema( - &self, - schema_params: &SettingsJsonSchemaParams, - cx: &App, - ) -> serde_json::Value { + pub fn json_schema(&self, schema_params: &SettingsJsonSchemaParams, cx: &App) -> Value { use schemars::{ r#gen::SchemaSettings, schema::{Schema, SchemaObject}, @@ -1101,7 +1161,7 @@ impl AnySettingValue for SettingValue { )?)) } - fn deserialize_setting(&self, mut json: &serde_json::Value) -> Result { + fn deserialize_setting(&self, mut json: &Value) -> Result { if let Some(key) = T::KEY { if let Some(value) = json.get(key) { json = value; @@ -1150,26 +1210,58 @@ impl AnySettingValue for SettingValue { ) -> RootSchema { T::json_schema(generator, params, cx) } + + fn edits_for_update( + &self, + raw_settings: &serde_json::Value, + tab_size: usize, + vscode_settings: &VsCodeSettings, + text: &mut String, + edits: &mut Vec<(Range, String)>, + ) { + let old_content = match self.deserialize_setting(raw_settings) { + Ok(content) => content.0.downcast::().unwrap(), + Err(_) => Box::<::FileContent>::default(), + }; + let mut new_content = old_content.clone(); + T::import_from_vscode(vscode_settings, &mut new_content); + + let old_value = serde_json::to_value(&old_content).unwrap(); + let new_value = serde_json::to_value(new_content).unwrap(); + + let mut key_path = Vec::new(); + if let Some(key) = T::KEY { + key_path.push(key); + } + + update_value_in_json_text( + text, + &mut key_path, + tab_size, + &old_value, + &new_value, + T::PRESERVED_KEYS.unwrap_or_default(), + edits, + ); + } } fn update_value_in_json_text<'a>( text: &mut String, key_path: &mut Vec<&'a str>, tab_size: usize, - old_value: &'a serde_json::Value, - new_value: &'a serde_json::Value, + old_value: &'a Value, + new_value: &'a Value, preserved_keys: &[&str], edits: &mut Vec<(Range, String)>, ) { // If the old and new values are both objects, then compare them key by key, // preserving the comments and formatting of the unchanged parts. Otherwise, // replace the old value with the new value. - if let (serde_json::Value::Object(old_object), serde_json::Value::Object(new_object)) = - (old_value, new_value) - { + if let (Value::Object(old_object), Value::Object(new_object)) = (old_value, new_value) { for (key, old_sub_value) in old_object.iter() { key_path.push(key); - let new_sub_value = new_object.get(key).unwrap_or(&serde_json::Value::Null); + let new_sub_value = new_object.get(key).unwrap_or(&Value::Null); update_value_in_json_text( text, key_path, @@ -1188,7 +1280,7 @@ fn update_value_in_json_text<'a>( text, key_path, tab_size, - &serde_json::Value::Null, + &Value::Null, new_sub_value, preserved_keys, edits, @@ -1215,7 +1307,7 @@ fn replace_value_in_json_text( text: &str, key_path: &[&str], tab_size: usize, - new_value: &serde_json::Value, + new_value: &Value, ) -> (Range, String) { static PAIR_QUERY: LazyLock = LazyLock::new(|| { Query::new( @@ -1667,6 +1759,165 @@ mod tests { ); } + #[gpui::test] + fn test_vscode_import(cx: &mut App) { + let mut store = SettingsStore::new(cx); + store.register_setting::(cx); + store.register_setting::(cx); + store.register_setting::(cx); + store.register_setting::(cx); + + // create settings that werent present + check_vscode_import( + &mut store, + r#"{ + } + "# + .unindent(), + r#" { "user.age": 37 } "#.to_owned(), + r#"{ + "user": { + "age": 37 + } + } + "# + .unindent(), + cx, + ); + + // persist settings that were present + check_vscode_import( + &mut store, + r#"{ + "user": { + "staff": true, + "age": 37 + } + } + "# + .unindent(), + r#"{ "user.age": 42 }"#.to_owned(), + r#"{ + "user": { + "staff": true, + "age": 42 + } + } + "# + .unindent(), + cx, + ); + + // don't clobber settings that aren't present in vscode + check_vscode_import( + &mut store, + r#"{ + "user": { + "staff": true, + "age": 37 + } + } + "# + .unindent(), + r#"{}"#.to_owned(), + r#"{ + "user": { + "staff": true, + "age": 37 + } + } + "# + .unindent(), + cx, + ); + + // custom enum + check_vscode_import( + &mut store, + r#"{ + "journal": { + "hour_format": "hour12" + } + } + "# + .unindent(), + r#"{ "time_format": "24" }"#.to_owned(), + r#"{ + "journal": { + "hour_format": "hour24" + } + } + "# + .unindent(), + cx, + ); + + // Multiple keys for one setting + check_vscode_import( + &mut store, + r#"{ + "key1": "value" + } + "# + .unindent(), + r#"{ + "key_1_first": "hello", + "key_1_second": "world" + }"# + .to_owned(), + r#"{ + "key1": "hello world" + } + "# + .unindent(), + cx, + ); + + // Merging lists together entries added and updated + check_vscode_import( + &mut store, + r#"{ + "languages": { + "JSON": { + "language_setting_1": true + }, + "Rust": { + "language_setting_2": true + } + } + }"# + .unindent(), + r#"{ + "vscode_languages": [ + { + "name": "JavaScript", + "language_setting_1": true + }, + { + "name": "Rust", + "language_setting_2": false + } + ] + }"# + .to_owned(), + r#"{ + "languages": { + "JavaScript": { + "language_setting_1": true + }, + "JSON": { + "language_setting_1": true + }, + "Rust": { + "language_setting_2": false + } + } + }"# + .unindent(), + cx, + ); + } + fn check_settings_update( store: &mut SettingsStore, old_json: String, @@ -1683,6 +1934,18 @@ mod tests { pretty_assertions::assert_eq!(new_json, expected_new_json); } + fn check_vscode_import( + store: &mut SettingsStore, + old: String, + vscode: String, + expected: String, + cx: &mut App, + ) { + store.set_user_settings(&old, cx).ok(); + let new = store.get_vscode_edits(old, &VsCodeSettings::from_str(&vscode).unwrap()); + pretty_assertions::assert_eq!(new, expected); + } + #[derive(Debug, PartialEq, Deserialize)] struct UserSettings { name: String, @@ -1704,6 +1967,10 @@ mod tests { fn load(sources: SettingsSources, _: &mut App) -> Result { sources.json_merge() } + + fn import_from_vscode(vscode: &VsCodeSettings, current: &mut Self::FileContent) { + vscode.u32_setting("user.age", &mut current.age); + } } #[derive(Debug, Deserialize, PartialEq)] @@ -1716,6 +1983,8 @@ mod tests { fn load(sources: SettingsSources, _: &mut App) -> Result { sources.json_merge() } + + fn import_from_vscode(_vscode: &VsCodeSettings, _current: &mut Self::FileContent) {} } #[derive(Clone, Debug, PartialEq, Deserialize)] @@ -1740,6 +2009,15 @@ mod tests { fn load(sources: SettingsSources, _: &mut App) -> Result { sources.json_merge() } + + fn import_from_vscode(vscode: &VsCodeSettings, current: &mut Self::FileContent) { + let first_value = vscode.read_string("key_1_first"); + let second_value = vscode.read_string("key_1_second"); + + if let Some((first, second)) = first_value.zip(second_value) { + current.key1 = Some(format!("{} {}", first, second)); + } + } } #[derive(Debug, Deserialize)] @@ -1769,6 +2047,14 @@ mod tests { fn load(sources: SettingsSources, _: &mut App) -> Result { sources.json_merge() } + + fn import_from_vscode(vscode: &VsCodeSettings, current: &mut Self::FileContent) { + vscode.enum_setting("time_format", &mut current.hour_format, |s| match s { + "12" => Some(HourFormat::Hour12), + "24" => Some(HourFormat::Hour24), + _ => None, + }); + } } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] @@ -1791,5 +2077,30 @@ mod tests { fn load(sources: SettingsSources, _: &mut App) -> Result { sources.json_merge() } + + fn import_from_vscode(vscode: &VsCodeSettings, current: &mut Self::FileContent) { + current.languages.extend( + vscode + .read_value("vscode_languages") + .and_then(|value| value.as_array()) + .map(|languages| { + languages + .iter() + .filter_map(|value| value.as_object()) + .filter_map(|item| { + let mut rest = item.clone(); + let name = rest.remove("name")?.as_str()?.to_string(); + let entry = serde_json::from_value::( + serde_json::Value::Object(rest), + ) + .ok()?; + + Some((name, entry)) + }) + }) + .into_iter() + .flatten(), + ); + } } } diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs new file mode 100644 index 0000000000..08a6b3e8d3 --- /dev/null +++ b/crates/settings/src/vscode_import.rs @@ -0,0 +1,88 @@ +use anyhow::Result; +use fs::Fs; +use serde_json::{Map, Value}; + +use std::sync::Arc; + +pub struct VsCodeSettings { + content: Map, +} + +impl VsCodeSettings { + pub fn from_str(content: &str) -> Result { + Ok(Self { + content: serde_json_lenient::from_str(content)?, + }) + } + + pub async fn load_user_settings(fs: Arc) -> Result { + let content = fs.load(paths::vscode_settings_file()).await?; + Ok(Self { + content: serde_json_lenient::from_str(&content)?, + }) + } + + pub fn read_value(&self, setting: &str) -> Option<&Value> { + if let Some(value) = self.content.get(setting) { + return Some(value); + } + // TODO: maybe check if it's in [platform] settings for current platform as a fallback + // TODO: deal with language specific settings + None + } + + pub fn read_string(&self, setting: &str) -> Option<&str> { + self.read_value(setting).and_then(|v| v.as_str()) + } + + pub fn read_bool(&self, setting: &str) -> Option { + self.read_value(setting).and_then(|v| v.as_bool()) + } + + pub fn string_setting(&self, key: &str, setting: &mut Option) { + if let Some(s) = self.content.get(key).and_then(Value::as_str) { + *setting = Some(s.to_owned()) + } + } + + pub fn bool_setting(&self, key: &str, setting: &mut Option) { + if let Some(s) = self.content.get(key).and_then(Value::as_bool) { + *setting = Some(s) + } + } + + pub fn u32_setting(&self, key: &str, setting: &mut Option) { + if let Some(s) = self.content.get(key).and_then(Value::as_u64) { + *setting = Some(s as u32) + } + } + + pub fn u64_setting(&self, key: &str, setting: &mut Option) { + if let Some(s) = self.content.get(key).and_then(Value::as_u64) { + *setting = Some(s) + } + } + + pub fn usize_setting(&self, key: &str, setting: &mut Option) { + if let Some(s) = self.content.get(key).and_then(Value::as_u64) { + *setting = Some(s.try_into().unwrap()) + } + } + + pub fn f32_setting(&self, key: &str, setting: &mut Option) { + if let Some(s) = self.content.get(key).and_then(Value::as_f64) { + *setting = Some(s as f32) + } + } + + pub fn enum_setting( + &self, + key: &str, + setting: &mut Option, + f: impl FnOnce(&str) -> Option, + ) { + if let Some(s) = self.content.get(key).and_then(Value::as_str).and_then(f) { + *setting = Some(s) + } + } +} diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 079ef0afb9..9eacacb9e0 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -15,9 +15,14 @@ path = "src/settings_ui.rs" command_palette_hooks.workspace = true editor.workspace = true feature_flags.workspace = true +fs.workspace = true gpui.workspace = true +log.workspace = true +paths.workspace = true settings.workspace = true theme.workspace = true ui.workspace = true workspace.workspace = true workspace-hack.workspace = true +serde.workspace = true +schemars.workspace = true diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 430997b6d3..65c420c6bf 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -5,7 +5,14 @@ use std::any::TypeId; use command_palette_hooks::CommandPaletteFilter; use editor::EditorSettingsControls; use feature_flags::{FeatureFlag, FeatureFlagViewExt}; -use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, actions}; +use fs::Fs; +use gpui::{ + App, AsyncWindowContext, Entity, EventEmitter, FocusHandle, Focusable, Task, actions, + impl_actions, +}; +use schemars::JsonSchema; +use serde::Deserialize; +use settings::SettingsStore; use ui::prelude::*; use workspace::Workspace; use workspace::item::{Item, ItemEvent}; @@ -18,6 +25,13 @@ impl FeatureFlag for SettingsUiFeatureFlag { const NAME: &'static str = "settings-ui"; } +#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema)] +pub struct ImportVsCodeSettings { + #[serde(default)] + pub skip_prompt: bool, +} + +impl_actions!(zed, [ImportVsCodeSettings]); actions!(zed, [OpenSettingsEditor]); pub fn init(cx: &mut App) { @@ -41,6 +55,59 @@ pub fn init(cx: &mut App) { } }); + workspace.register_action(|_workspace, action: &ImportVsCodeSettings, window, cx| { + let fs = ::global(cx); + let action = *action; + + window + .spawn(cx, async move |cx: &mut AsyncWindowContext| { + let vscode = + match settings::VsCodeSettings::load_user_settings(fs.clone()).await { + Ok(vscode) => vscode, + Err(err) => { + println!( + "Failed to load VsCode settings: {}", + err.context(format!( + "Loading VsCode settings from path: {:?}", + paths::vscode_settings_file() + )) + ); + + let _ = cx.prompt( + gpui::PromptLevel::Info, + "Could not find or load a VsCode settings file", + None, + &["Ok"], + ); + return; + } + }; + + let prompt = if action.skip_prompt { + Task::ready(Some(0)) + } else { + let prompt = cx.prompt( + gpui::PromptLevel::Warning, + "Importing settings may overwrite your existing settings", + None, + &["Ok", "Cancel"], + ); + cx.spawn(async move |_| prompt.await.ok()) + }; + if prompt.await != Some(0) { + return; + } + + cx.update(|_, cx| { + cx.global::() + .import_vscode_settings(fs, vscode); + log::info!("Imported settings from VsCode"); + }) + .ok(); + }) + .detach(); + }); + let settings_ui_actions = [TypeId::of::()]; CommandPaletteFilter::update_global(cx, |filter, _cx| { diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index 29be1144fe..589dd67996 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -255,6 +255,70 @@ impl settings::Settings for TerminalSettings { root_schema } + + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + let name = |s| format!("terminal.integrated.{s}"); + + vscode.f32_setting(&name("fontSize"), &mut current.font_size); + vscode.string_setting(&name("fontFamily"), &mut current.font_family); + vscode.bool_setting(&name("copyOnSelection"), &mut current.copy_on_select); + vscode.bool_setting("macOptionIsMeta", &mut current.option_as_meta); + vscode.usize_setting("scrollback", &mut current.max_scroll_history_lines); + match vscode.read_bool(&name("cursorBlinking")) { + Some(true) => current.blinking = Some(TerminalBlink::On), + Some(false) => current.blinking = Some(TerminalBlink::Off), + None => {} + } + vscode.enum_setting( + &name("cursorStyle"), + &mut current.cursor_shape, + |s| match s { + "block" => Some(CursorShape::Block), + "line" => Some(CursorShape::Bar), + "underline" => Some(CursorShape::Underline), + _ => None, + }, + ); + // they also have "none" and "outline" as options but just for the "Inactive" variant + if let Some(height) = vscode + .read_value(&name("lineHeight")) + .and_then(|v| v.as_f64()) + { + current.line_height = Some(TerminalLineHeight::Custom(height as f32)) + } + + #[cfg(target_os = "windows")] + let platform = "windows"; + #[cfg(target_os = "linux")] + let platform = "linux"; + #[cfg(target_os = "macos")] + let platform = "osx"; + + // TODO: handle arguments + let shell_name = format!("{platform}Exec"); + if let Some(s) = vscode.read_string(&name(&shell_name)) { + current.shell = Some(Shell::Program(s.to_owned())) + } + + if let Some(env) = vscode + .read_value(&name(&format!("env.{platform}"))) + .and_then(|v| v.as_object()) + { + for (k, v) in env { + if v.is_null() { + if let Some(zed_env) = current.env.as_mut() { + zed_env.remove(k); + } + } + let Some(v) = v.as_str() else { continue }; + if let Some(zed_env) = current.env.as_mut() { + zed_env.insert(k.clone(), v.to_owned()); + } else { + current.env = Some([(k.clone(), v.to_owned())].into_iter().collect()) + } + } + } + } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 99480d04a7..837abaee60 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -950,6 +950,13 @@ impl settings::Settings for ThemeSettings { root_schema } + + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + vscode.f32_setting("editor.fontWeight", &mut current.buffer_font_weight); + vscode.f32_setting("editor.fontSize", &mut current.buffer_font_size); + vscode.string_setting("editor.font", &mut current.buffer_font_family); + // TODO: possibly map editor.fontLigatures to buffer_font_features? + } } fn merge(target: &mut T, value: Option) { diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index dc2a0edc17..096fdab333 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1798,4 +1798,8 @@ impl Settings for VimSettings { cursor_shape: settings.cursor_shape.ok_or_else(Self::missing_default)?, }) } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) { + // TODO: translate vim extension settings + } } diff --git a/crates/vim_mode_setting/src/vim_mode_setting.rs b/crates/vim_mode_setting/src/vim_mode_setting.rs index e3f1f4b946..e2aa2e9c23 100644 --- a/crates/vim_mode_setting/src/vim_mode_setting.rs +++ b/crates/vim_mode_setting/src/vim_mode_setting.rs @@ -33,4 +33,8 @@ impl Settings for VimModeSetting { .unwrap_or(sources.default.ok_or_else(Self::missing_default)?), )) } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) { + // TODO: could possibly check if any of the `vim.` keys are set? + } } diff --git a/crates/welcome/src/base_keymap_setting.rs b/crates/welcome/src/base_keymap_setting.rs index fb9a3080c0..bce4d78617 100644 --- a/crates/welcome/src/base_keymap_setting.rs +++ b/crates/welcome/src/base_keymap_setting.rs @@ -107,4 +107,8 @@ impl Settings for BaseKeymap { } sources.default.ok_or_else(Self::missing_default) } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + *current = Some(BaseKeymap::VSCode); + } } diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index f1a8abfd6b..540a3de990 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -201,7 +201,8 @@ impl Render for WelcomePage { zed_actions::OpenSettings, ), cx); })), - ), + ) + ) .child( v_flex() diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 594d7eeca3..6fef4d10ea 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -141,6 +141,35 @@ impl Settings for ItemSettings { fn load(sources: SettingsSources, _: &mut App) -> Result { sources.json_merge() } + + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + if let Some(b) = vscode.read_bool("workbench.editor.tabActionCloseVisibility") { + current.show_close_button = Some(if b { + ShowCloseButton::Always + } else { + ShowCloseButton::Hidden + }) + } + vscode.enum_setting( + "workbench.editor.tabActionLocation", + &mut current.close_position, + |s| match s { + "right" => Some(ClosePosition::Right), + "left" => Some(ClosePosition::Left), + _ => None, + }, + ); + if let Some(b) = vscode.read_bool("workbench.editor.focusRecentEditorAfterClose") { + current.activate_on_close = Some(if b { + ActivateOnClose::History + } else { + ActivateOnClose::LeftNeighbour + }) + } + + vscode.bool_setting("workbench.editor.showIcons", &mut current.file_icons); + vscode.bool_setting("git.decorations.enabled", &mut current.git_status); + } } impl Settings for PreviewTabsSettings { @@ -151,6 +180,18 @@ impl Settings for PreviewTabsSettings { fn load(sources: SettingsSources, _: &mut App) -> Result { sources.json_merge() } + + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + vscode.bool_setting("workbench.editor.enablePreview", &mut current.enabled); + vscode.bool_setting( + "workbench.editor.enablePreviewFromCodeNavigation", + &mut current.enable_preview_from_code_navigation, + ); + vscode.bool_setting( + "workbench.editor.enablePreviewFromQuickOpen", + &mut current.enable_preview_from_file_finder, + ); + } } #[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index a61a987b1c..85753b98bf 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -48,7 +48,7 @@ impl OnLastWindowClosed { } } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct ActivePanelModifiers { /// Scale by which to zoom the active pane. @@ -277,6 +277,89 @@ impl Settings for WorkspaceSettings { fn load(sources: SettingsSources, _: &mut App) -> Result { sources.json_merge() } + + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + if vscode + .read_bool("accessibility.dimUnfocused.enabled") + .unwrap_or_default() + { + if let Some(opacity) = vscode + .read_value("accessibility.dimUnfocused.opacity") + .and_then(|v| v.as_f64()) + { + if let Some(settings) = current.active_pane_modifiers.as_mut() { + settings.inactive_opacity = Some(opacity as f32) + } else { + current.active_pane_modifiers = Some(ActivePanelModifiers { + inactive_opacity: Some(opacity as f32), + ..Default::default() + }) + } + } + } + + vscode.enum_setting( + "window.confirmBeforeClose", + &mut current.confirm_quit, + |s| match s { + "always" | "keyboardOnly" => Some(true), + "never" => Some(false), + _ => None, + }, + ); + + vscode.bool_setting( + "workbench.editor.restoreViewState", + &mut current.restore_on_file_reopen, + ); + + if let Some(b) = vscode.read_bool("window.closeWhenEmpty") { + current.when_closing_with_no_tabs = Some(if b { + CloseWindowWhenNoItems::CloseWindow + } else { + CloseWindowWhenNoItems::KeepWindowOpen + }) + } + + if let Some(b) = vscode.read_bool("files.simpleDialog.enable") { + current.use_system_path_prompts = Some(!b); + } + + vscode.enum_setting("files.autoSave", &mut current.autosave, |s| match s { + "off" => Some(AutosaveSetting::Off), + "afterDelay" => Some(AutosaveSetting::AfterDelay { + milliseconds: vscode + .read_value("files.autoSaveDelay") + .and_then(|v| v.as_u64()) + .unwrap_or(1000), + }), + "onFocusChange" => Some(AutosaveSetting::OnFocusChange), + "onWindowChange" => Some(AutosaveSetting::OnWindowChange), + _ => None, + }); + + // workbench.editor.limit contains "enabled", "value", and "perEditorGroup" + // our semantics match if those are set to true, some N, and true respectively. + // we'll ignore "perEditorGroup" for now since we only support a global max + if let Some(n) = vscode + .read_value("workbench.editor.limit.value") + .and_then(|v| v.as_u64()) + .and_then(|n| NonZeroUsize::new(n as usize)) + { + if vscode + .read_bool("workbench.editor.limit.enabled") + .unwrap_or_default() + { + current.max_tabs = Some(n) + } + } + + // some combination of "window.restoreWindows" and "workbench.startupEditor" might + // map to our "restore_on_startup" + + // there doesn't seem to be a way to read whether the bottom dock's "justified" + // setting is enabled in vscode. that'd be our equivalent to "bottom_dock_layout" + } } impl Settings for TabBarSettings { @@ -287,4 +370,19 @@ impl Settings for TabBarSettings { fn load(sources: SettingsSources, _: &mut App) -> Result { sources.json_merge() } + + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + vscode.enum_setting( + "workbench.editor.showTabs", + &mut current.show, + |s| match s { + "multiple" => Some(true), + "single" | "none" => Some(false), + _ => None, + }, + ); + if Some("hidden") == vscode.read_string("workbench.editor.editorActionsLocation") { + current.show_tab_bar_buttons = Some(false) + } + } } diff --git a/crates/worktree/src/worktree_settings.rs b/crates/worktree/src/worktree_settings.rs index d13a1b8928..26cf16e8f6 100644 --- a/crates/worktree/src/worktree_settings.rs +++ b/crates/worktree/src/worktree_settings.rs @@ -96,6 +96,31 @@ impl Settings for WorktreeSettings { )?, }) } + + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + if let Some(inclusions) = vscode + .read_value("files.watcherInclude") + .and_then(|v| v.as_array()) + .and_then(|v| v.iter().map(|n| n.as_str().map(str::to_owned)).collect()) + { + if let Some(old) = current.file_scan_inclusions.as_mut() { + old.extend(inclusions) + } else { + current.file_scan_inclusions = Some(inclusions) + } + } + if let Some(exclusions) = vscode + .read_value("files.watcherExclude") + .and_then(|v| v.as_array()) + .and_then(|v| v.iter().map(|n| n.as_str().map(str::to_owned)).collect()) + { + if let Some(old) = current.file_scan_exclusions.as_mut() { + old.extend(exclusions) + } else { + current.file_scan_exclusions = Some(exclusions) + } + } + } } fn path_matchers(values: &[String], context: &'static str) -> anyhow::Result { diff --git a/crates/zlog_settings/src/zlog_settings.rs b/crates/zlog_settings/src/zlog_settings.rs index fde28ba918..b58cbcc143 100644 --- a/crates/zlog_settings/src/zlog_settings.rs +++ b/crates/zlog_settings/src/zlog_settings.rs @@ -32,4 +32,6 @@ impl Settings for ZlogSettings { { sources.json_merge() } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} }