Add basic proxy settings (#11852)

张小白 and Jason Lee created

Adding `proxy` keyword to configure proxy while using zed. After setting
the proxy, restart Zed to acctually use the proxy.

Example setting: 
```rust
"proxy" = "socks5://localhost:10808"
"proxy" = "http://127.0.0.1:10809"
```

Closes #9424, closes #9422, closes #8650, closes #5032, closes #6701,
closes #11890

Release Notes:

- Added settings to configure proxy in Zed

---------

Co-authored-by: Jason Lee <huacnlee@gmail.com>

Change summary

assets/settings/default.json              | 16 +++++
crates/client/src/client.rs               | 27 +++++++++++
crates/extension/src/extension_builder.rs |  2 
crates/gpui/src/app.rs                    |  7 ++
crates/http/src/http.rs                   | 60 +++++++++++++++++++-----
crates/node_runtime/src/node_runtime.rs   | 15 +++++
crates/semantic_index/examples/index.rs   |  2 
crates/zed/src/main.rs                    |  1 
8 files changed, 110 insertions(+), 20 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -797,5 +797,17 @@
   //   - `short`: "2 s, 15 l, 32 c"
   //   - `long`: "2 selections, 15 lines, 32 characters"
   // Default: long
-  "line_indicator_format": "long"
-}
+  "line_indicator_format": "long",
+  // Set a proxy to use. The proxy protocol is specified by the URI scheme.
+  //
+  // Supported URI scheme: `http`, `https`, `socks4`, `socks4a`, `socks5`,
+  // `socks5h`. `http` will be used when no scheme is specified.
+  // 
+  // By default no proxy will be used, or Zed will try get proxy settings from
+  // environment variables.
+  //
+  // Examples:
+  //   - "proxy" = "socks5://localhost:10808"
+  //   - "proxy" = "http://127.0.0.1:10809"
+  "proxy": null
+}

crates/client/src/client.rs 🔗

@@ -114,10 +114,36 @@ impl Settings for ClientSettings {
     }
 }
 
+#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
+pub struct ProxySettingsContent {
+    proxy: Option<String>,
+}
+
+#[derive(Deserialize, Default)]
+pub struct ProxySettings {
+    pub proxy: Option<String>,
+}
+
+impl Settings for ProxySettings {
+    const KEY: Option<&'static str> = None;
+
+    type FileContent = ProxySettingsContent;
+
+    fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
+        Ok(Self {
+            proxy: sources
+                .user
+                .and_then(|value| value.proxy.clone())
+                .or(sources.default.proxy.clone()),
+        })
+    }
+}
+
 pub fn init_settings(cx: &mut AppContext) {
     TelemetrySettings::register(cx);
     cx.update_global(|store: &mut SettingsStore, cx| {
         store.register_setting::<ClientSettings>(cx);
+        store.register_setting::<ProxySettings>(cx);
     });
 }
 
@@ -512,6 +538,7 @@ impl Client {
         let clock = Arc::new(clock::RealSystemClock);
         let http = Arc::new(HttpClientWithUrl::new(
             &ClientSettings::get_global(cx).server_url,
+            ProxySettings::get_global(cx).proxy.clone(),
         ));
         Self::new(clock, http.clone(), cx)
     }

crates/gpui/src/app.rs 🔗

@@ -115,7 +115,7 @@ impl App {
         Self(AppContext::new(
             current_platform(),
             Arc::new(()),
-            http::client(),
+            http::client(None),
         ))
     }
 
@@ -651,6 +651,11 @@ impl AppContext {
         self.platform.local_timezone()
     }
 
+    /// Updates the http client assigned to GPUI
+    pub fn update_http_client(&mut self, new_client: Arc<dyn HttpClient>) {
+        self.http_client = new_client;
+    }
+
     /// Returns the http client assigned to GPUI
     pub fn http_client(&self) -> Arc<dyn HttpClient> {
         self.http_client.clone()

crates/http/src/http.rs 🔗

@@ -16,7 +16,7 @@ use std::{
 };
 pub use url::Url;
 
-fn http_proxy_from_env() -> Option<isahc::http::Uri> {
+fn get_proxy(proxy: Option<String>) -> Option<isahc::http::Uri> {
     macro_rules! try_env {
         ($($env:literal),+) => {
             $(
@@ -27,29 +27,42 @@ fn http_proxy_from_env() -> Option<isahc::http::Uri> {
         };
     }
 
-    try_env!(
-        "ALL_PROXY",
-        "all_proxy",
-        "HTTPS_PROXY",
-        "https_proxy",
-        "HTTP_PROXY",
-        "http_proxy"
-    );
-    None
+    proxy
+        .and_then(|input| {
+            input
+                .parse::<isahc::http::Uri>()
+                .inspect_err(|e| log::error!("Error parsing proxy settings: {}", e))
+                .ok()
+        })
+        .or_else(|| {
+            try_env!(
+                "ALL_PROXY",
+                "all_proxy",
+                "HTTPS_PROXY",
+                "https_proxy",
+                "HTTP_PROXY",
+                "http_proxy"
+            );
+            None
+        })
 }
 
 /// An [`HttpClient`] that has a base URL.
 pub struct HttpClientWithUrl {
     base_url: Mutex<String>,
     client: Arc<dyn HttpClient>,
+    proxy: Option<String>,
 }
 
 impl HttpClientWithUrl {
     /// Returns a new [`HttpClientWithUrl`] with the given base URL.
-    pub fn new(base_url: impl Into<String>) -> Self {
+    pub fn new(base_url: impl Into<String>, unparsed_proxy: Option<String>) -> Self {
+        let parsed_proxy = get_proxy(unparsed_proxy);
+        let proxy_string = parsed_proxy.as_ref().map(|p| p.to_string());
         Self {
             base_url: Mutex::new(base_url.into()),
-            client: client(),
+            client: client(parsed_proxy),
+            proxy: proxy_string,
         }
     }
 
@@ -100,6 +113,10 @@ impl HttpClient for Arc<HttpClientWithUrl> {
     ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
         self.client.send(req)
     }
+
+    fn proxy(&self) -> Option<&str> {
+        self.proxy.as_deref()
+    }
 }
 
 impl HttpClient for HttpClientWithUrl {
@@ -109,6 +126,10 @@ impl HttpClient for HttpClientWithUrl {
     ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
         self.client.send(req)
     }
+
+    fn proxy(&self) -> Option<&str> {
+        self.proxy.as_deref()
+    }
 }
 
 pub trait HttpClient: Send + Sync {
@@ -153,14 +174,16 @@ pub trait HttpClient: Send + Sync {
             Err(error) => async move { Err(error.into()) }.boxed(),
         }
     }
+
+    fn proxy(&self) -> Option<&str>;
 }
 
-pub fn client() -> Arc<dyn HttpClient> {
+pub fn client(proxy: Option<isahc::http::Uri>) -> Arc<dyn HttpClient> {
     Arc::new(
         isahc::HttpClient::builder()
             .connect_timeout(Duration::from_secs(5))
             .low_speed_timeout(100, Duration::from_secs(5))
-            .proxy(http_proxy_from_env())
+            .proxy(proxy)
             .build()
             .unwrap(),
     )
@@ -174,6 +197,10 @@ impl HttpClient for isahc::HttpClient {
         let client = self.clone();
         Box::pin(async move { client.send_async(req).await })
     }
+
+    fn proxy(&self) -> Option<&str> {
+        None
+    }
 }
 
 #[cfg(feature = "test-support")]
@@ -201,6 +228,7 @@ impl FakeHttpClient {
             client: Arc::new(Self {
                 handler: Box::new(move |req| Box::pin(handler(req))),
             }),
+            proxy: None,
         })
     }
 
@@ -239,4 +267,8 @@ impl HttpClient for FakeHttpClient {
         let future = (self.handler)(req);
         Box::pin(async move { future.await.map(Into::into) })
     }
+
+    fn proxy(&self) -> Option<&str> {
+        None
+    }
 }

crates/node_runtime/src/node_runtime.rs 🔗

@@ -266,8 +266,21 @@ impl NodeRuntime for RealNodeRuntime {
                 command.args(["--prefix".into(), directory.to_path_buf()]);
             }
 
+            if let Some(proxy) = self.http.proxy() {
+                command.args(["--proxy", proxy]);
+            }
+
             #[cfg(windows)]
-            command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
+            {
+                // SYSTEMROOT is a critical environment variables for Windows.
+                if let Some(val) = std::env::var("SYSTEMROOT")
+                    .context("Missing environment variable: SYSTEMROOT!")
+                    .log_err()
+                {
+                    command.env("SYSTEMROOT", val);
+                }
+                command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
+            }
 
             command.output().await.map_err(|e| anyhow!("{e}"))
         };

crates/semantic_index/examples/index.rs 🔗

@@ -26,7 +26,7 @@ fn main() {
         });
 
         let clock = Arc::new(FakeSystemClock::default());
-        let http = Arc::new(HttpClientWithUrl::new("http://localhost:11434"));
+        let http = Arc::new(HttpClientWithUrl::new("http://localhost:11434", None));
 
         let client = client::Client::new(clock, http.clone(), cx);
         Client::set_global(client.clone(), cx);

crates/zed/src/main.rs 🔗

@@ -343,6 +343,7 @@ fn main() {
 
         client::init_settings(cx);
         let client = Client::production(cx);
+        cx.update_http_client(client.http_client().clone());
         let mut languages =
             LanguageRegistry::new(login_shell_env_loaded, cx.background_executor().clone());
         languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());