nightly url setting (#7037)

Conrad Irwin created

Release Notes:

- Added the ability to set settings per-release stage
- Added a `"server_url"` setting

Change summary

Cargo.lock                                |  1 
assets/settings/default.json              | 23 +++++++
crates/auto_update/src/auto_update.rs     | 38 +++++------
crates/channel/src/channel_store_tests.rs |  1 
crates/client/src/client.rs               | 80 +++++++++++++++++-------
crates/client/src/telemetry.rs            | 17 +---
crates/collab/src/tests/test_server.rs    |  1 
crates/feedback/src/feedback_modal.rs     |  6 
crates/settings/src/settings_store.rs     | 42 ++++++++++++
crates/util/Cargo.toml                    |  1 
crates/util/src/http.rs                   | 50 ++++++++++++++-
crates/zed/src/main.rs                    | 33 +++++++--
12 files changed, 217 insertions(+), 76 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -9025,6 +9025,7 @@ dependencies = [
  "isahc",
  "lazy_static",
  "log",
+ "parking_lot 0.11.2",
  "rand 0.8.5",
  "rust-embed",
  "serde",

assets/settings/default.json 🔗

@@ -503,5 +503,28 @@
     //         }
     //     }
     // }
+  },
+  // The server to connect to. If the environment variable
+  // ZED_SERVER_URL is set, it will override this setting.
+  "server_url": "https://zed.dev",
+  // Settings overrides to use when using Zed Preview.
+  // Mostly useful for developers who are managing multiple instances of Zed.
+  "preview": {
+    // "theme": "Andromeda"
+  },
+  // Settings overrides to use when using Zed Nightly.
+  // Mostly useful for developers who are managing multiple instances of Zed.
+  "nightly": {
+    // "theme": "Andromeda"
+  },
+  // Settings overrides to use when using Zed Stable.
+  // Mostly useful for developers who are managing multiple instances of Zed.
+  "stable": {
+    // "theme": "Andromeda"
+  },
+  // Settings overrides to use when using Zed Stable.
+  // Mostly useful for developers who are managing multiple instances of Zed.
+  "dev": {
+    // "theme": "Andromeda"
   }
 }

crates/auto_update/src/auto_update.rs 🔗

@@ -25,8 +25,11 @@ use std::{
     time::Duration,
 };
 use update_notification::UpdateNotification;
-use util::channel::{AppCommitSha, ReleaseChannel};
 use util::http::HttpClient;
+use util::{
+    channel::{AppCommitSha, ReleaseChannel},
+    http::ZedHttpClient,
+};
 use workspace::Workspace;
 
 const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
@@ -54,9 +57,8 @@ pub enum AutoUpdateStatus {
 pub struct AutoUpdater {
     status: AutoUpdateStatus,
     current_version: SemanticVersion,
-    http_client: Arc<dyn HttpClient>,
+    http_client: Arc<ZedHttpClient>,
     pending_poll: Option<Task<Option<()>>>,
-    server_url: String,
 }
 
 #[derive(Deserialize)]
@@ -92,7 +94,7 @@ impl Settings for AutoUpdateSetting {
     }
 }
 
-pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) {
+pub fn init(http_client: Arc<ZedHttpClient>, cx: &mut AppContext) {
     AutoUpdateSetting::register(cx);
 
     cx.observe_new_views(|workspace: &mut Workspace, _cx| {
@@ -106,7 +108,7 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
 
     if let Some(version) = ZED_APP_VERSION.or_else(|| cx.app_metadata().app_version) {
         let auto_updater = cx.new_model(|cx| {
-            let updater = AutoUpdater::new(version, http_client, server_url);
+            let updater = AutoUpdater::new(version, http_client);
 
             let mut update_subscription = AutoUpdateSetting::get_global(cx)
                 .0
@@ -151,10 +153,11 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<(
         ReleaseChannel::Stable | ReleaseChannel::Preview
     ) {
         let auto_updater = auto_updater.read(cx);
-        let server_url = &auto_updater.server_url;
         let release_channel = release_channel.dev_name();
         let current_version = auto_updater.current_version;
-        let url = format!("{server_url}/releases/{release_channel}/{current_version}");
+        let url = &auto_updater
+            .http_client
+            .zed_url(&format!("/releases/{release_channel}/{current_version}"));
         cx.open_url(&url);
     }
 
@@ -191,16 +194,11 @@ impl AutoUpdater {
         cx.default_global::<Option<Model<Self>>>().clone()
     }
 
-    fn new(
-        current_version: SemanticVersion,
-        http_client: Arc<dyn HttpClient>,
-        server_url: String,
-    ) -> Self {
+    fn new(current_version: SemanticVersion, http_client: Arc<ZedHttpClient>) -> Self {
         Self {
             status: AutoUpdateStatus::Idle,
             current_version,
             http_client,
-            server_url,
             pending_poll: None,
         }
     }
@@ -246,18 +244,14 @@ impl AutoUpdater {
     }
 
     async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
-        let (client, server_url, current_version) = this.read_with(&cx, |this, _| {
-            (
-                this.http_client.clone(),
-                this.server_url.clone(),
-                this.current_version,
-            )
+        let (client, current_version) = this.read_with(&cx, |this, _| {
+            (this.http_client.clone(), this.current_version)
         })?;
 
-        let mut url_string = format!(
-            "{server_url}/api/releases/latest?asset=Zed.dmg&os={}&arch={}",
+        let mut url_string = client.zed_url(&format!(
+            "/api/releases/latest?asset=Zed.dmg&os={}&arch={}",
             OS, ARCH
-        );
+        ));
         cx.update(|cx| {
             if let Some(param) = cx
                 .try_global::<ReleaseChannel>()

crates/channel/src/channel_store_tests.rs 🔗

@@ -329,6 +329,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
 fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
     let settings_store = SettingsStore::test(cx);
     cx.set_global(settings_store);
+    client::init_settings(cx);
 
     let http = FakeHttpClient::with_404_response();
     let client = Client::new(http.clone(), cx);

crates/client/src/client.rs 🔗

@@ -26,7 +26,7 @@ use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, Requ
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use serde_json;
-use settings::Settings;
+use settings::{Settings, SettingsStore};
 use std::{
     any::TypeId,
     collections::HashMap,
@@ -41,8 +41,8 @@ use std::{
 use telemetry::Telemetry;
 use thiserror::Error;
 use url::Url;
-use util::channel::ReleaseChannel;
 use util::http::HttpClient;
+use util::{channel::ReleaseChannel, http::ZedHttpClient};
 use util::{ResultExt, TryFutureExt};
 
 pub use rpc::*;
@@ -50,9 +50,8 @@ pub use telemetry::Event;
 pub use user::*;
 
 lazy_static! {
-    pub static ref ZED_SERVER_URL: String =
-        std::env::var("ZED_SERVER_URL").unwrap_or_else(|_| "https://zed.dev".to_string());
-    pub static ref ZED_RPC_URL: Option<String> = std::env::var("ZED_RPC_URL").ok();
+    static ref ZED_SERVER_URL: Option<String> = std::env::var("ZED_SERVER_URL").ok();
+    static ref ZED_RPC_URL: Option<String> = std::env::var("ZED_RPC_URL").ok();
     pub static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
         .ok()
         .and_then(|s| if s.is_empty() { None } else { Some(s) });
@@ -73,13 +72,45 @@ pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
 
 actions!(client, [SignIn, SignOut, Reconnect]);
 
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+pub struct ClientSettingsContent {
+    server_url: Option<String>,
+}
+
+#[derive(Deserialize)]
+pub struct ClientSettings {
+    pub server_url: String,
+}
+
+impl Settings for ClientSettings {
+    const KEY: Option<&'static str> = None;
+
+    type FileContent = ClientSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &mut AppContext,
+    ) -> Result<Self>
+    where
+        Self: Sized,
+    {
+        let mut result = Self::load_via_json_merge(default_value, user_values)?;
+        if let Some(server_url) = &*ZED_SERVER_URL {
+            result.server_url = server_url.clone()
+        }
+        Ok(result)
+    }
+}
+
 pub fn init_settings(cx: &mut AppContext) {
     TelemetrySettings::register(cx);
+    cx.update_global(|store: &mut SettingsStore, cx| {
+        store.register_setting::<ClientSettings>(cx);
+    });
 }
 
 pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
-    init_settings(cx);
-
     let client = Arc::downgrade(client);
     cx.on_action({
         let client = client.clone();
@@ -121,7 +152,7 @@ pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
 pub struct Client {
     id: AtomicU64,
     peer: Arc<Peer>,
-    http: Arc<dyn HttpClient>,
+    http: Arc<ZedHttpClient>,
     telemetry: Arc<Telemetry>,
     state: RwLock<ClientState>,
 
@@ -390,8 +421,8 @@ impl settings::Settings for TelemetrySettings {
 }
 
 impl Client {
-    pub fn new(http: Arc<dyn HttpClient>, cx: &mut AppContext) -> Arc<Self> {
-        Arc::new(Self {
+    pub fn new(http: Arc<ZedHttpClient>, cx: &mut AppContext) -> Arc<Self> {
+        let client = Arc::new(Self {
             id: AtomicU64::new(0),
             peer: Peer::new(0),
             telemetry: Telemetry::new(http.clone(), cx),
@@ -402,14 +433,16 @@ impl Client {
             authenticate: Default::default(),
             #[cfg(any(test, feature = "test-support"))]
             establish_connection: Default::default(),
-        })
+        });
+
+        client
     }
 
     pub fn id(&self) -> u64 {
         self.id.load(std::sync::atomic::Ordering::SeqCst)
     }
 
-    pub fn http_client(&self) -> Arc<dyn HttpClient> {
+    pub fn http_client(&self) -> Arc<ZedHttpClient> {
         self.http.clone()
     }
 
@@ -925,14 +958,14 @@ impl Client {
     }
 
     async fn get_rpc_url(
-        http: Arc<dyn HttpClient>,
+        http: Arc<ZedHttpClient>,
         release_channel: Option<ReleaseChannel>,
     ) -> Result<Url> {
         if let Some(url) = &*ZED_RPC_URL {
             return Url::parse(url).context("invalid rpc url");
         }
 
-        let mut url = format!("{}/rpc", *ZED_SERVER_URL);
+        let mut url = http.zed_url("/rpc");
         if let Some(preview_param) =
             release_channel.and_then(|channel| channel.release_query_param())
         {
@@ -1053,10 +1086,10 @@ impl Client {
 
                     // Open the Zed sign-in page in the user's browser, with query parameters that indicate
                     // that the user is signing in from a Zed app running on the same device.
-                    let mut url = format!(
-                        "{}/native_app_signin?native_app_port={}&native_app_public_key={}",
-                        *ZED_SERVER_URL, port, public_key_string
-                    );
+                    let mut url = http.zed_url(&format!(
+                        "/native_app_signin?native_app_port={}&native_app_public_key={}",
+                        port, public_key_string
+                    ));
 
                     if let Some(impersonate_login) = IMPERSONATE_LOGIN.as_ref() {
                         log::info!("impersonating user @{}", impersonate_login);
@@ -1088,7 +1121,7 @@ impl Client {
                                     }
 
                                     let post_auth_url =
-                                        format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL);
+                                        http.zed_url("/native_app_signin_succeeded");
                                     req.respond(
                                         tiny_http::Response::empty(302).with_header(
                                             tiny_http::Header::from_bytes(
@@ -1130,7 +1163,7 @@ impl Client {
     }
 
     async fn authenticate_as_admin(
-        http: Arc<dyn HttpClient>,
+        http: Arc<ZedHttpClient>,
         login: String,
         mut api_token: String,
     ) -> Result<Credentials> {
@@ -1351,7 +1384,7 @@ async fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credenti
     }
 
     let (user_id, access_token) = cx
-        .update(|cx| cx.read_credentials(&ZED_SERVER_URL))
+        .update(|cx| cx.read_credentials(&ClientSettings::get_global(cx).server_url))
         .log_err()?
         .await
         .log_err()??;
@@ -1368,7 +1401,7 @@ async fn write_credentials_to_keychain(
 ) -> Result<()> {
     cx.update(move |cx| {
         cx.write_credentials(
-            &ZED_SERVER_URL,
+            &ClientSettings::get_global(cx).server_url,
             &credentials.user_id.to_string(),
             credentials.access_token.as_bytes(),
         )
@@ -1377,7 +1410,7 @@ async fn write_credentials_to_keychain(
 }
 
 async fn delete_credentials_from_keychain(cx: &AsyncAppContext) -> Result<()> {
-    cx.update(move |cx| cx.delete_credentials(&ZED_SERVER_URL))?
+    cx.update(move |cx| cx.delete_credentials(&ClientSettings::get_global(cx).server_url))?
         .await
 }
 
@@ -1684,6 +1717,7 @@ mod tests {
         cx.update(|cx| {
             let settings_store = SettingsStore::test(cx);
             cx.set_global(settings_store);
+            init_settings(cx);
         });
     }
 }

crates/client/src/telemetry.rs 🔗

@@ -1,10 +1,9 @@
 mod event_coalescer;
 
-use crate::{TelemetrySettings, ZED_SERVER_URL};
+use crate::TelemetrySettings;
 use chrono::{DateTime, Utc};
 use futures::Future;
 use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
-use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use serde::Serialize;
 use settings::{Settings, SettingsStore};
@@ -13,7 +12,7 @@ use sysinfo::{
     CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt,
 };
 use tempfile::NamedTempFile;
-use util::http::HttpClient;
+use util::http::{HttpClient, ZedHttpClient};
 #[cfg(not(debug_assertions))]
 use util::ResultExt;
 use util::{channel::ReleaseChannel, TryFutureExt};
@@ -21,7 +20,7 @@ use util::{channel::ReleaseChannel, TryFutureExt};
 use self::event_coalescer::EventCoalescer;
 
 pub struct Telemetry {
-    http_client: Arc<dyn HttpClient>,
+    http_client: Arc<ZedHttpClient>,
     executor: BackgroundExecutor,
     state: Arc<Mutex<TelemetryState>>,
 }
@@ -43,12 +42,6 @@ struct TelemetryState {
     max_queue_size: usize,
 }
 
-const EVENTS_URL_PATH: &'static str = "/api/events";
-
-lazy_static! {
-    static ref EVENTS_URL: String = format!("{}{}", *ZED_SERVER_URL, EVENTS_URL_PATH);
-}
-
 #[derive(Serialize, Debug)]
 struct EventRequestBody {
     installation_id: Option<Arc<str>>,
@@ -149,7 +142,7 @@ const FLUSH_INTERVAL: Duration = Duration::from_secs(1);
 const FLUSH_INTERVAL: Duration = Duration::from_secs(60 * 5);
 
 impl Telemetry {
-    pub fn new(client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Arc<Self> {
+    pub fn new(client: Arc<ZedHttpClient>, cx: &mut AppContext) -> Arc<Self> {
         let release_channel = cx
             .try_global::<ReleaseChannel>()
             .map(|release_channel| release_channel.display_name());
@@ -548,7 +541,7 @@ impl Telemetry {
                     }
 
                     this.http_client
-                        .post_json(EVENTS_URL.as_str(), json_bytes.into())
+                        .post_json(&this.http_client.zed_url("/api/events"), json_bytes.into())
                         .await?;
                     anyhow::Ok(())
                 }

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

@@ -153,6 +153,7 @@ impl TestServer {
             }
             let settings = SettingsStore::test(cx);
             cx.set_global(settings);
+            client::init_settings(cx);
         });
 
         let http = FakeHttpClient::with_404_response();

crates/feedback/src/feedback_modal.rs 🔗

@@ -2,7 +2,7 @@ use std::{ops::RangeInclusive, sync::Arc, time::Duration};
 
 use anyhow::{anyhow, bail};
 use bitflags::bitflags;
-use client::{Client, ZED_SERVER_URL};
+use client::Client;
 use db::kvp::KEY_VALUE_STORE;
 use editor::{Editor, EditorEvent};
 use futures::AsyncReadExt;
@@ -16,7 +16,7 @@ use project::Project;
 use regex::Regex;
 use serde_derive::Serialize;
 use ui::{prelude::*, Button, ButtonStyle, IconPosition, Tooltip};
-use util::ResultExt;
+use util::{http::HttpClient, ResultExt};
 use workspace::{ModalView, Toast, Workspace};
 
 use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedRepo};
@@ -293,12 +293,12 @@ impl FeedbackModal {
             }
         }
 
-        let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL);
         let telemetry = zed_client.telemetry();
         let metrics_id = telemetry.metrics_id();
         let installation_id = telemetry.installation_id();
         let is_staff = telemetry.is_staff();
         let http_client = zed_client.http_client();
+        let feedback_endpoint = http_client.zed_url("/api/feedback");
         let request = FeedbackRequestBody {
             feedback_text: &feedback_text,
             email,

crates/settings/src/settings_store.rs 🔗

@@ -1,6 +1,6 @@
 use anyhow::{anyhow, Context, Result};
 use collections::{btree_map, hash_map, BTreeMap, HashMap};
-use gpui::AppContext;
+use gpui::{AppContext, AsyncAppContext};
 use lazy_static::lazy_static;
 use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema};
 use serde::{de::DeserializeOwned, Deserialize as _, Serialize};
@@ -13,7 +13,9 @@ use std::{
     str,
     sync::Arc,
 };
-use util::{merge_non_null_json_value_into, RangeExt, ResultExt as _};
+use util::{
+    channel::RELEASE_CHANNEL_NAME, merge_non_null_json_value_into, RangeExt, ResultExt as _,
+};
 
 /// A value that can be defined as a user setting.
 ///
@@ -102,6 +104,14 @@ pub trait Settings: 'static + Send + Sync {
         cx.global::<SettingsStore>().get(None)
     }
 
+    #[track_caller]
+    fn try_read_global<'a, R>(cx: &'a AsyncAppContext, f: impl FnOnce(&Self) -> R) -> Option<R>
+    where
+        Self: Sized,
+    {
+        cx.try_read_global(|s: &SettingsStore, _| f(s.get(None)))
+    }
+
     #[track_caller]
     fn override_global<'a>(settings: Self, cx: &'a mut AppContext)
     where
@@ -197,6 +207,15 @@ impl SettingsStore {
                 user_values_stack = vec![user_settings];
             }
 
+            if let Some(release_settings) = &self.raw_user_settings.get(&*RELEASE_CHANNEL_NAME) {
+                if let Some(release_settings) = setting_value
+                    .deserialize_setting(&release_settings)
+                    .log_err()
+                {
+                    user_values_stack.push(release_settings);
+                }
+            }
+
             if let Some(setting) = setting_value
                 .load_setting(&default_settings, &user_values_stack, cx)
                 .context("A default setting must be added to the `default.json` file")
@@ -484,6 +503,15 @@ impl SettingsStore {
             }
         }
 
+        for release_stage in ["dev", "nightly", "stable", "preview"] {
+            let schema = combined_schema.schema.clone();
+            combined_schema
+                .schema
+                .object()
+                .properties
+                .insert(release_stage.to_string(), schema.into());
+        }
+
         serde_json::to_value(&combined_schema).unwrap()
     }
 
@@ -509,6 +537,16 @@ impl SettingsStore {
                 paths_stack.push(None);
             }
 
+            if let Some(release_settings) = &self.raw_user_settings.get(&*RELEASE_CHANNEL_NAME) {
+                if let Some(release_settings) = setting_value
+                    .deserialize_setting(&release_settings)
+                    .log_err()
+                {
+                    user_settings_stack.push(release_settings);
+                    paths_stack.push(None);
+                }
+            }
+
             // If the global settings file changed, reload the global value for the field.
             if changed_local_path.is_none() {
                 if let Some(value) = setting_value

crates/util/Cargo.toml 🔗

@@ -30,6 +30,7 @@ serde_json.workspace = true
 git2 = { workspace = true, optional = true }
 dirs = "3.0"
 take-until = "0.2.0"
+parking_lot.workspace = true
 
 [target.'cfg(windows)'.dependencies]
 tendril = "0.4.3"

crates/util/src/http.rs 🔗

@@ -6,12 +6,49 @@ pub use isahc::{
     Error,
 };
 pub use isahc::{AsyncBody, Request, Response};
+use parking_lot::Mutex;
 use smol::future::FutureExt;
 #[cfg(feature = "test-support")]
 use std::fmt;
 use std::{sync::Arc, time::Duration};
 pub use url::Url;
 
+pub struct ZedHttpClient {
+    pub zed_host: Mutex<String>,
+    client: Box<dyn HttpClient>,
+}
+
+impl ZedHttpClient {
+    pub fn zed_url(&self, path: &str) -> String {
+        format!("{}{}", self.zed_host.lock(), path)
+    }
+}
+
+impl HttpClient for Arc<ZedHttpClient> {
+    fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>> {
+        self.client.send(req)
+    }
+}
+
+impl HttpClient for ZedHttpClient {
+    fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>> {
+        self.client.send(req)
+    }
+}
+
+pub fn zed_client(zed_host: &str) -> Arc<ZedHttpClient> {
+    Arc::new(ZedHttpClient {
+        zed_host: Mutex::new(zed_host.to_string()),
+        client: Box::new(
+            isahc::HttpClient::builder()
+                .connect_timeout(Duration::from_secs(5))
+                .low_speed_timeout(100, Duration::from_secs(5))
+                .build()
+                .unwrap(),
+        ),
+    })
+}
+
 pub trait HttpClient: Send + Sync {
     fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>>;
 
@@ -81,17 +118,20 @@ pub struct FakeHttpClient {
 
 #[cfg(feature = "test-support")]
 impl FakeHttpClient {
-    pub fn create<Fut, F>(handler: F) -> Arc<dyn HttpClient>
+    pub fn create<Fut, F>(handler: F) -> Arc<ZedHttpClient>
     where
         Fut: 'static + Send + futures::Future<Output = Result<Response<AsyncBody>, Error>>,
         F: 'static + Send + Sync + Fn(Request<AsyncBody>) -> Fut,
     {
-        Arc::new(Self {
-            handler: Box::new(move |req| Box::pin(handler(req))),
+        Arc::new(ZedHttpClient {
+            zed_host: Mutex::new("http://test.example".into()),
+            client: Box::new(Self {
+                handler: Box::new(move |req| Box::pin(handler(req))),
+            }),
         })
     }
 
-    pub fn with_404_response() -> Arc<dyn HttpClient> {
+    pub fn with_404_response() -> Arc<ZedHttpClient> {
         Self::create(|_| async move {
             Ok(Response::builder()
                 .status(404)
@@ -100,7 +140,7 @@ impl FakeHttpClient {
         })
     }
 
-    pub fn with_200_response() -> Arc<dyn HttpClient> {
+    pub fn with_200_response() -> Arc<ZedHttpClient> {
         Self::create(|_| async move {
             Ok(Response::builder()
                 .status(200)

crates/zed/src/main.rs 🔗

@@ -42,7 +42,7 @@ use theme::{ActiveTheme, ThemeRegistry, ThemeSettings};
 use util::{
     async_maybe,
     channel::{parse_zed_link, AppCommitSha, ReleaseChannel, RELEASE_CHANNEL},
-    http::{self, HttpClient},
+    http::{self, HttpClient, ZedHttpClient},
     paths::{self, CRASHES_DIR, CRASHES_RETIRED_DIR},
     ResultExt,
 };
@@ -59,7 +59,6 @@ fn main() {
     menu::init();
     zed_actions::init();
 
-    let http = http::client();
     init_paths();
     init_logger();
 
@@ -132,6 +131,9 @@ fn main() {
         cx.set_global(store);
         handle_settings_file_changes(user_settings_file_rx, cx);
         handle_keymap_file_changes(user_keymap_file_rx, cx);
+        client::init_settings(cx);
+
+        let http = http::zed_client(&client::ClientSettings::get_global(cx).server_url);
 
         let client = client::Client::new(http.clone(), cx);
         let mut languages = LanguageRegistry::new(login_shell_env_loaded);
@@ -201,7 +203,20 @@ fn main() {
         languages.set_theme(cx.theme().clone());
         cx.observe_global::<SettingsStore>({
             let languages = languages.clone();
-            move |cx| languages.set_theme(cx.theme().clone())
+            let http = http.clone();
+            let client = client.clone();
+
+            move |cx| {
+                languages.set_theme(cx.theme().clone());
+                let new_host = &client::ClientSettings::get_global(cx).server_url;
+                let mut host = http.zed_host.lock();
+                if &*host != new_host {
+                    *host = new_host.clone();
+                    if client.status().borrow().is_connected() {
+                        client.reconnect(&cx.to_async());
+                    }
+                }
+            }
         })
         .detach();
 
@@ -230,7 +245,7 @@ fn main() {
         cx.set_global(Arc::downgrade(&app_state));
 
         audio::init(Assets, cx);
-        auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx);
+        auto_update::init(http.clone(), cx);
 
         workspace::init(app_state.clone(), cx);
         recent_projects::init(cx);
@@ -634,7 +649,7 @@ fn init_panic_hook(app: &App, installation_id: Option<String>, session_id: Strin
     }));
 }
 
-fn upload_panics_and_crashes(http: Arc<dyn HttpClient>, cx: &mut AppContext) {
+fn upload_panics_and_crashes(http: Arc<ZedHttpClient>, cx: &mut AppContext) {
     let telemetry_settings = *client::TelemetrySettings::get_global(cx);
     cx.background_executor()
         .spawn(async move {
@@ -650,10 +665,10 @@ fn upload_panics_and_crashes(http: Arc<dyn HttpClient>, cx: &mut AppContext) {
 
 /// upload panics to us (via zed.dev)
 async fn upload_previous_panics(
-    http: Arc<dyn HttpClient>,
+    http: Arc<ZedHttpClient>,
     telemetry_settings: client::TelemetrySettings,
 ) -> Result<()> {
-    let panic_report_url = format!("{}/api/panic", &*client::ZED_SERVER_URL);
+    let panic_report_url = http.zed_url("/api/panic");
     let mut children = smol::fs::read_dir(&*paths::LOGS_DIR).await?;
     while let Some(child) = children.next().await {
         let child = child?;
@@ -717,7 +732,7 @@ static LAST_CRASH_UPLOADED: &'static str = "LAST_CRASH_UPLOADED";
 /// upload crashes from apple's diagnostic reports to our server.
 /// (only if telemetry is enabled)
 async fn upload_previous_crashes(
-    http: Arc<dyn HttpClient>,
+    http: Arc<ZedHttpClient>,
     telemetry_settings: client::TelemetrySettings,
 ) -> Result<()> {
     if !telemetry_settings.diagnostics {
@@ -728,7 +743,7 @@ async fn upload_previous_crashes(
         .unwrap_or("zed-2024-01-17-221900.ips".to_string()); // don't upload old crash reports from before we had this.
     let mut uploaded = last_uploaded.clone();
 
-    let crash_report_url = format!("{}/api/crash", &*client::ZED_SERVER_URL);
+    let crash_report_url = http.zed_url("/api/crash");
 
     for dir in [&*CRASHES_DIR, &*CRASHES_RETIRED_DIR] {
         let mut children = smol::fs::read_dir(&dir).await?;