recent projects

Conrad Irwin created

Change summary

Cargo.lock                                             |  1 
crates/agent_settings/src/agent_settings.rs            |  6 
crates/agent_ui/src/agent_configuration.rs             | 35 +++-
crates/agent_ui/src/agent_configuration/tool_picker.rs | 19 +-
crates/agent_ui/src/agent_panel.rs                     | 41 ++---
crates/agent_ui/src/text_thread_editor.rs              | 22 ++
crates/collab/src/tests/editor_tests.rs                | 20 +-
crates/outline_panel/src/outline_panel.rs              | 18 -
crates/recent_projects/src/recent_projects.rs          |  9 
crates/recent_projects/src/remote_connections.rs       | 86 ++---------
crates/recent_projects/src/remote_servers.rs           | 39 ++---
crates/remote/Cargo.toml                               |  1 
crates/remote/src/transport/ssh.rs                     | 24 +-
crates/settings/src/settings_content.rs                | 52 +++++++
crates/settings/src/settings_content/project.rs        |  2 
crates/settings_ui/src/settings_ui.rs                  |  2 
16 files changed, 195 insertions(+), 182 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -13507,6 +13507,7 @@ dependencies = [
  "schemars",
  "serde",
  "serde_json",
+ "settings",
  "shlex",
  "smol",
  "tempfile",

crates/agent_settings/src/agent_settings.rs 🔗

@@ -8,8 +8,8 @@ use language_model::LanguageModel;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{
-    DefaultAgentView, LanguageModelParameters, LanguageModelSelection, NotifyWhenAgentWaiting,
-    Settings, SettingsContent,
+    DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection,
+    NotifyWhenAgentWaiting, Settings, SettingsContent,
 };
 use util::MergeFrom;
 
@@ -24,7 +24,7 @@ pub fn init(cx: &mut App) {
     AgentSettings::register(cx);
 }
 
-#[derive(Default, Clone, Debug)]
+#[derive(Clone, Debug)]
 pub struct AgentSettings {
     pub enabled: bool,
     pub button: bool,

crates/agent_ui/src/agent_configuration.rs 🔗

@@ -413,8 +413,8 @@ impl AgentConfiguration {
             always_allow_tool_actions,
             move |state, _window, cx| {
                 let allow = state == &ToggleState::Selected;
-                update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
-                    settings.set_always_allow_tool_actions(allow);
+                update_settings_file(fs.clone(), cx, move |settings, _| {
+                    settings.agent.get_or_insert_default.set_always_allow_tool_actions(allow);
                 });
             },
         )
@@ -431,8 +431,11 @@ impl AgentConfiguration {
             single_file_review,
             move |state, _window, cx| {
                 let allow = state == &ToggleState::Selected;
-                update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
-                    settings.set_single_file_review(allow);
+                update_settings_file(fs.clone(), cx, move |settings, _| {
+                    settings
+                        .agent
+                        .get_or_insert_default()
+                        .set_single_file_review(allow);
                 });
             },
         )
@@ -1279,7 +1282,7 @@ async fn open_new_agent_servers_entry_in_settings_editor(
             let settings = cx.global::<SettingsStore>();
 
             let mut unique_server_name = None;
-            let edits = settings.edits_for_update::<AllAgentServersSettings>(&text, |file| {
+            let edits = settings.edits_for_update(&text, |settings| {
                 let server_name: Option<SharedString> = (0..u8::MAX)
                     .map(|i| {
                         if i == 0 {
@@ -1288,20 +1291,26 @@ async fn open_new_agent_servers_entry_in_settings_editor(
                             format!("your_agent_{}", i).into()
                         }
                     })
-                    .find(|name| !file.custom.contains_key(name));
+                    .find(|name| {
+                        !settings
+                            .agent_servers
+                            .is_some_and(|agent_servers| agent_servers.custom.contains_key(name))
+                    });
                 if let Some(server_name) = server_name {
                     unique_server_name = Some(server_name.clone());
-                    file.custom.insert(
-                        server_name,
-                        CustomAgentServerSettings {
-                            command: AgentServerCommand {
+                    settings
+                        .agent_servers
+                        .get_or_insert_default()
+                        .custom
+                        .insert(
+                            server_name,
+                            settings::CustomAgentServerSettings {
                                 path: "path_to_executable".into(),
                                 args: vec![],
                                 env: Some(HashMap::default()),
+                                default_mode: None,
                             },
-                            default_mode: None,
-                        },
-                    );
+                        );
                 }
             });
 

crates/agent_ui/src/agent_configuration/tool_picker.rs 🔗

@@ -1,14 +1,11 @@
 use std::{collections::BTreeMap, sync::Arc};
 
-use agent_settings::{
-    AgentProfileContent, AgentProfileId, AgentProfileSettings, AgentSettings, AgentSettingsContent,
-    ContextServerPresetContent,
-};
+use agent_settings::{AgentProfileId, AgentProfileSettings};
 use assistant_tool::{ToolSource, ToolWorkingSet};
 use fs::Fs;
 use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
 use picker::{Picker, PickerDelegate};
-use settings::update_settings_file;
+use settings::{AgentProfileContent, ContextServerPresetContent, update_settings_file};
 use ui::{ListItem, ListItemSpacing, prelude::*};
 use util::ResultExt as _;
 
@@ -266,15 +263,19 @@ impl PickerDelegate for ToolPickerDelegate {
             is_enabled
         };
 
-        update_settings_file::<AgentSettings>(self.fs.clone(), cx, {
+        update_settings_file(self.fs.clone(), cx, {
             let profile_id = self.profile_id.clone();
             let default_profile = self.profile_settings.clone();
             let server_id = server_id.clone();
             let tool_name = tool_name.clone();
-            move |settings: &mut AgentSettingsContent, _cx| {
-                let profiles = settings.profiles.get_or_insert_default();
+            move |settings, _cx| {
+                let profiles = settings
+                    .agent
+                    .get_or_insert_default()
+                    .profiles
+                    .get_or_insert_default();
                 let profile = profiles
-                    .entry(profile_id)
+                    .entry(profile_id.0)
                     .or_insert_with(|| AgentProfileContent {
                         name: default_profile.name.into(),
                         tools: default_profile.tools,

crates/agent_ui/src/agent_panel.rs 🔗

@@ -1059,17 +1059,14 @@ impl AgentPanel {
         match self.active_view.which_font_size_used() {
             WhichFontSize::AgentFont => {
                 if persist {
-                    update_settings_file::<ThemeSettings>(
-                        self.fs.clone(),
-                        cx,
-                        move |settings, cx| {
-                            let agent_font_size =
-                                ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
-                            let _ = settings
-                                .agent_font_size
-                                .insert(Some(theme::clamp_font_size(agent_font_size).into()));
-                        },
-                    );
+                    update_settings_file(self.fs.clone(), cx, move |settings, cx| {
+                        let agent_font_size =
+                            ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
+                        let _ = settings
+                            .theme
+                            .agent_font_size
+                            .insert(Some(theme::clamp_font_size(agent_font_size).into()));
+                    });
                 } else {
                     theme::adjust_agent_font_size(cx, |size| size + delta);
                 }
@@ -1176,11 +1173,9 @@ impl AgentPanel {
                     .is_none_or(|model| model.provider.id() != provider.id())
                     && let Some(model) = provider.default_model(cx)
                 {
-                    update_settings_file::<AgentSettings>(
-                        self.fs.clone(),
-                        cx,
-                        move |settings, _| settings.set_model(model),
-                    );
+                    update_settings_file(self.fs.clone(), cx, move |settings, _| {
+                        settings.agent.get_or_insert_default().set_model(model)
+                    });
                 }
 
                 self.new_thread(&NewThread::default(), window, cx);
@@ -1425,7 +1420,7 @@ impl Focusable for AgentPanel {
 }
 
 fn agent_panel_dock_position(cx: &App) -> DockPosition {
-    AgentSettings::get_global(cx).dock
+    AgentSettings::get_global(cx).dock.into()
 }
 
 impl EventEmitter<PanelEvent> for AgentPanel {}
@@ -1444,13 +1439,11 @@ impl Panel for AgentPanel {
     }
 
     fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
-        settings::update_settings_file::<AgentSettings>(self.fs.clone(), cx, move |settings, _| {
-            let dock = match position {
-                DockPosition::Left => AgentDockPosition::Left,
-                DockPosition::Bottom => AgentDockPosition::Bottom,
-                DockPosition::Right => AgentDockPosition::Right,
-            };
-            settings.set_dock(dock);
+        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
+            settings
+                .agent
+                .get_or_insert_default()
+                .set_dock(position.into());
         });
     }
 

crates/agent_ui/src/text_thread_editor.rs 🔗

@@ -3,7 +3,7 @@ use crate::{
     language_model_selector::{LanguageModelSelector, language_model_selector},
     ui::BurnModeTooltip,
 };
-use agent_settings::{AgentSettings, CompletionMode};
+use agent_settings::CompletionMode;
 use anyhow::Result;
 use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
 use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases};
@@ -41,7 +41,10 @@ use project::{Project, Worktree};
 use project::{ProjectPath, lsp_store::LocalLspAdapterDelegate};
 use rope::Point;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsStore, update_settings_file};
+use settings::{
+    LanguageModelProviderSetting, LanguageModelSelection, Settings, SettingsStore,
+    update_settings_file,
+};
 use std::{
     any::TypeId,
     cmp,
@@ -294,11 +297,16 @@ impl TextThreadEditor {
                 language_model_selector(
                     |cx| LanguageModelRegistry::read_global(cx).default_model(),
                     move |model, cx| {
-                        update_settings_file::<AgentSettings>(
-                            fs.clone(),
-                            cx,
-                            move |settings, _| settings.set_model(model.clone()),
-                        );
+                        update_settings_file(fs.clone(), cx, move |settings, _| {
+                            let provider = model.provider_id().0.to_string();
+                            let model = model.id().0.to_string();
+                            settings.agent.get_or_insert_default().set_model(
+                                LanguageModelSelection {
+                                    provider: LanguageModelProviderSetting(provider),
+                                    model: model.clone(),
+                                },
+                            )
+                        });
                     },
                     window,
                     cx,

crates/collab/src/tests/editor_tests.rs 🔗

@@ -1789,8 +1789,8 @@ async fn test_mutual_editor_inlay_hint_cache_update(
 
     cx_a.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
-                settings.defaults.inlay_hints = Some(InlayHintSettings {
+            store.update_user_settings(cx, |settings| {
+                settings.project.all_languages.defaults.inlay_hints = Some(InlayHintSettings {
                     enabled: true,
                     show_value_hints: true,
                     edit_debounce_ms: 0,
@@ -1806,8 +1806,8 @@ async fn test_mutual_editor_inlay_hint_cache_update(
     });
     cx_b.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
-                settings.defaults.inlay_hints = Some(InlayHintSettings {
+            store.update_user_settings(cx, |settings| {
+                settings.project.all_languages.defaults.inlay_hints = Some(InlayHintSettings {
                     show_value_hints: true,
                     enabled: true,
                     edit_debounce_ms: 0,
@@ -2039,8 +2039,8 @@ async fn test_inlay_hint_refresh_is_forwarded(
 
     cx_a.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
-                settings.defaults.inlay_hints = Some(InlayHintSettings {
+            store.update_user_settings(cx, |settings| {
+                settings.project.all_languages.defaults.inlay_hints = Some(InlayHintSettings {
                     show_value_hints: true,
                     enabled: false,
                     edit_debounce_ms: 0,
@@ -2056,8 +2056,8 @@ async fn test_inlay_hint_refresh_is_forwarded(
     });
     cx_b.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
-                settings.defaults.inlay_hints = Some(InlayHintSettings {
+            store.update_user_settings(cx, |settings| {
+                settings.project.all_languages.defaults.inlay_hints = Some(InlayHintSettings {
                     show_value_hints: true,
                     enabled: true,
                     edit_debounce_ms: 0,
@@ -2242,14 +2242,14 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
     cx_a.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
             store.update_user_settings::<EditorSettings>(cx, |settings| {
-                settings.lsp_document_colors = Some(DocumentColorsRenderMode::None);
+                settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::None);
             });
         });
     });
     cx_b.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
             store.update_user_settings::<EditorSettings>(cx, |settings| {
-                settings.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
+                settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
             });
         });
     });

crates/outline_panel/src/outline_panel.rs 🔗

@@ -4846,17 +4846,13 @@ impl Panel for OutlinePanel {
     }
 
     fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
-        settings::update_settings_file::<OutlinePanelSettings>(
-            self.fs.clone(),
-            cx,
-            move |settings, _| {
-                let dock = match position {
-                    DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left,
-                    DockPosition::Right => OutlinePanelDockPosition::Right,
-                };
-                settings.dock = Some(dock);
-            },
-        );
+        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
+            let dock = match position {
+                DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left,
+                DockPosition::Right => OutlinePanelDockPosition::Right,
+            };
+            settings.outline_panel.get_or_insert_default().dock = Some(dock);
+        });
     }
 
     fn size(&self, _: &Window, cx: &App) -> Pixels {

crates/recent_projects/src/recent_projects.rs 🔗

@@ -626,7 +626,7 @@ mod tests {
     use dap::debugger_settings::DebuggerSettings;
     use editor::Editor;
     use gpui::{TestAppContext, UpdateGlobal, WindowHandle};
-    use project::{Project, project_settings::ProjectSettings};
+    use project::Project;
     use serde_json::json;
     use settings::SettingsStore;
     use util::path;
@@ -640,8 +640,11 @@ mod tests {
 
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<ProjectSettings>(cx, |settings| {
-                    settings.session.restore_unsaved_buffers = false
+                store.update_user_settings(cx, |settings| {
+                    settings
+                        .session
+                        .get_or_insert_default()
+                        .restore_unsaved_buffers = Some(false)
                 });
             });
         });

crates/recent_projects/src/remote_connections.rs 🔗

@@ -1,4 +1,3 @@
-use std::collections::BTreeSet;
 use std::{path::PathBuf, sync::Arc};
 
 use anyhow::{Context as _, Result};
@@ -17,30 +16,26 @@ use markdown::{Markdown, MarkdownElement, MarkdownStyle};
 use release_channel::ReleaseChannel;
 use remote::{
     ConnectionIdentifier, RemoteClient, RemoteConnectionOptions, RemotePlatform,
-    SshConnectionOptions, SshPortForwardOption,
+    SshConnectionOptions,
 };
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+use settings::Settings;
+pub use settings::SshConnection;
 use theme::ThemeSettings;
 use ui::{
     ActiveTheme, Color, CommonAnimationExt, Context, Icon, IconName, IconSize, InteractiveElement,
     IntoElement, Label, LabelCommon, Styled, Window, prelude::*,
 };
-use util::serde::default_true;
+use util::MergeFrom;
 use workspace::{AppState, ModalView, Workspace};
 
-#[derive(Deserialize)]
 pub struct SshSettings {
-    pub ssh_connections: Option<Vec<SshConnection>>,
-    /// Whether to read ~/.ssh/config for ssh connection sources.
-    #[serde(default = "default_true")]
+    pub ssh_connections: Vec<SshConnection>,
     pub read_ssh_config: bool,
 }
 
 impl SshSettings {
     pub fn ssh_connections(&self) -> impl Iterator<Item = SshConnection> + use<> {
-        self.ssh_connections.clone().into_iter().flatten()
+        self.ssh_connections.clone().into_iter()
     }
 
     pub fn fill_connection_options_from_settings(&self, options: &mut SshConnectionOptions) {
@@ -75,67 +70,22 @@ impl SshSettings {
     }
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema)]
-pub struct SshConnection {
-    pub host: SharedString,
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub username: Option<String>,
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub port: Option<u16>,
-    #[serde(skip_serializing_if = "Vec::is_empty")]
-    #[serde(default)]
-    pub args: Vec<String>,
-    #[serde(default)]
-    pub projects: BTreeSet<SshProject>,
-    /// Name to use for this server in UI.
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub nickname: Option<String>,
-    // By default Zed will download the binary to the host directly.
-    // If this is set to true, Zed will download the binary to your local machine,
-    // and then upload it over the SSH connection. Useful if your SSH server has
-    // limited outbound internet access.
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub upload_binary_over_ssh: Option<bool>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub port_forwards: Option<Vec<SshPortForwardOption>>,
-}
-
-impl From<SshConnection> for SshConnectionOptions {
-    fn from(val: SshConnection) -> Self {
-        SshConnectionOptions {
-            host: val.host.into(),
-            username: val.username,
-            port: val.port,
-            password: None,
-            args: Some(val.args),
-            nickname: val.nickname,
-            upload_binary_over_ssh: val.upload_binary_over_ssh.unwrap_or_default(),
-            port_forwards: val.port_forwards,
+impl Settings for SshSettings {
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        let remote = &content.remote;
+        Self {
+            ssh_connections: remote.ssh_connections.clone().unwrap_or_default(),
+            read_ssh_config: remote.read_ssh_config.unwrap(),
         }
     }
-}
-
-#[derive(Clone, Default, Serialize, PartialEq, Eq, PartialOrd, Ord, Deserialize, JsonSchema)]
-pub struct SshProject {
-    pub paths: Vec<String>,
-}
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
-#[settings_key(None)]
-pub struct RemoteSettingsContent {
-    pub ssh_connections: Option<Vec<SshConnection>>,
-    pub read_ssh_config: Option<bool>,
-}
-
-impl Settings for SshSettings {
-    type FileContent = RemoteSettingsContent;
-
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        sources.json_merge()
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        if let Some(ssh_connections) = content.remote.ssh_connections.clone() {
+            self.ssh_connections.extend(ssh_connections)
+        }
+        self.read_ssh_config
+            .merge_from(&content.remote.read_ssh_config);
     }
-
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
 }
 
 pub struct RemoteConnectionPrompt {

crates/recent_projects/src/remote_servers.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     remote_connections::{
-        RemoteConnectionModal, RemoteConnectionPrompt, RemoteSettingsContent, SshConnection,
-        SshConnectionHeader, SshProject, SshSettings, connect_over_ssh, open_remote_project,
+        RemoteConnectionModal, RemoteConnectionPrompt, SshConnection, SshConnectionHeader,
+        SshSettings, connect_over_ssh, open_remote_project,
     },
     ssh_config::parse_ssh_config_hosts,
 };
@@ -20,7 +20,10 @@ use remote::{
     RemoteClient, RemoteConnectionOptions, SshConnectionOptions,
     remote_client::ConnectionIdentifier,
 };
-use settings::{Settings, SettingsStore, update_settings_file, watch_config_file};
+use settings::{
+    RemoteSettingsContent, Settings, SettingsStore, SshProject, update_settings_file,
+    watch_config_file,
+};
 use smol::stream::StreamExt as _;
 use std::{
     borrow::Cow,
@@ -173,13 +176,14 @@ impl ProjectPicker {
 
                     cx.update(|_, cx| {
                         let fs = app_state.fs.clone();
-                        update_settings_file::<SshSettings>(fs, cx, {
+                        update_settings_file(fs, cx, {
                             let paths = paths
                                 .iter()
                                 .map(|path| path.to_string_lossy().to_string())
                                 .collect();
                             move |setting, _| {
                                 if let Some(server) = setting
+                                    .remote
                                     .ssh_connections
                                     .as_mut()
                                     .and_then(|connections| connections.get_mut(ix))
@@ -987,7 +991,7 @@ impl RemoteServerProjects {
         else {
             return;
         };
-        update_settings_file::<SshSettings>(fs, cx, move |setting, cx| f(setting, cx));
+        update_settings_file(fs, cx, move |setting, cx| f(&mut setting.remote, cx));
     }
 
     fn delete_ssh_server(&mut self, server: usize, cx: &mut Context<Self>) {
@@ -1403,24 +1407,15 @@ impl RemoteServerProjects {
         cx: &mut Context<Self>,
     ) -> impl IntoElement {
         let ssh_settings = SshSettings::get_global(cx);
-        let mut should_rebuild = false;
-
-        if ssh_settings
-            .ssh_connections
-            .as_ref()
-            .is_some_and(|connections| {
-                state
-                    .servers
-                    .iter()
-                    .filter_map(|server| match server {
-                        RemoteEntry::Project { connection, .. } => Some(connection),
-                        RemoteEntry::SshConfig { .. } => None,
-                    })
-                    .ne(connections.iter())
+
+        let mut should_rebuild = state
+            .servers
+            .iter()
+            .filter_map(|server| match server {
+                RemoteEntry::Project { connection, .. } => Some(connection),
+                RemoteEntry::SshConfig { .. } => None,
             })
-        {
-            should_rebuild = true;
-        };
+            .ne(&ssh_settings.ssh_connections);
 
         if !should_rebuild && ssh_settings.read_ssh_config {
             let current_ssh_hosts: BTreeSet<SharedString> = state

crates/remote/Cargo.toml 🔗

@@ -35,6 +35,7 @@ rpc = { workspace = true, features = ["gpui"] }
 schemars.workspace =  true
 serde.workspace = true
 serde_json.workspace = true
+settings.workspace = true
 shlex.workspace = true
 smol.workspace = true
 tempfile.workspace = true

crates/remote/src/transport/ssh.rs 🔗

@@ -15,8 +15,7 @@ use itertools::Itertools;
 use parking_lot::Mutex;
 use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
 use rpc::proto::Envelope;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
+pub use settings::SshPortForwardOption;
 use smol::{
     fs,
     process::{self, Child, Stdio},
@@ -53,14 +52,19 @@ pub struct SshConnectionOptions {
     pub upload_binary_over_ssh: bool,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
-pub struct SshPortForwardOption {
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub local_host: Option<String>,
-    pub local_port: u16,
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub remote_host: Option<String>,
-    pub remote_port: u16,
+impl From<settings::SshConnection> for SshConnectionOptions {
+    fn from(val: settings::SshConnection) -> Self {
+        SshConnectionOptions {
+            host: val.host.into(),
+            username: val.username,
+            port: val.port,
+            password: None,
+            args: Some(val.args),
+            nickname: val.nickname,
+            upload_binary_over_ssh: val.upload_binary_over_ssh.unwrap_or_default(),
+            port_forwards: val.port_forwards,
+        }
+    }
 }
 
 #[derive(Clone)]

crates/settings/src/settings_content.rs 🔗

@@ -44,6 +44,9 @@ pub struct SettingsContent {
     #[serde(flatten)]
     pub editor: EditorSettingsContent,
 
+    #[serde(flatten)]
+    pub remote: RemoteSettingsContent,
+
     /// Settings related to the file finder.
     pub file_finder: Option<FileFinderSettingsContent>,
 
@@ -712,3 +715,52 @@ pub enum ImageFileSizeUnit {
     /// Displays file size in decimal units (e.g., KB, MB).
     Decimal,
 }
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+pub struct RemoteSettingsContent {
+    pub ssh_connections: Option<Vec<SshConnection>>,
+    pub read_ssh_config: Option<bool>,
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, JsonSchema)]
+pub struct SshConnection {
+    pub host: SharedString,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub username: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub port: Option<u16>,
+    #[serde(skip_serializing_if = "Vec::is_empty")]
+    #[serde(default)]
+    pub args: Vec<String>,
+    #[serde(default)]
+    pub projects: collections::BTreeSet<SshProject>,
+    /// Name to use for this server in UI.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub nickname: Option<String>,
+    // By default Zed will download the binary to the host directly.
+    // If this is set to true, Zed will download the binary to your local machine,
+    // and then upload it over the SSH connection. Useful if your SSH server has
+    // limited outbound internet access.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub upload_binary_over_ssh: Option<bool>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub port_forwards: Option<Vec<SshPortForwardOption>>,
+}
+
+#[derive(
+    Clone, Debug, Default, Serialize, PartialEq, Eq, PartialOrd, Ord, Deserialize, JsonSchema,
+)]
+pub struct SshProject {
+    pub paths: Vec<String>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
+pub struct SshPortForwardOption {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub local_host: Option<String>,
+    pub local_port: u16,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub remote_host: Option<String>,
+    pub remote_port: u16,
+}

crates/settings/src/settings_content/project.rs 🔗

@@ -139,7 +139,7 @@ pub struct DapSettings {
     pub args: Vec<String>,
 }
 
-#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)]
+#[derive(Default, Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)]
 pub struct SessionSettingsContent {
     /// Whether or not to restore unsaved buffers on restart.
     ///

crates/settings_ui/src/settings_ui.rs 🔗

@@ -5,7 +5,7 @@ use std::{
 };
 
 use anyhow::Context as _;
-use editor::{Editor, EditorSettingsControls};
+use editor::Editor;
 use feature_flags::{FeatureFlag, FeatureFlagAppExt};
 use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, ScrollHandle, actions};
 use settings::{