Make remote projects to sync in local user settings (#37560)

Kirill Bulatov created

Closes https://github.com/zed-industries/zed/issues/20024
Closes https://github.com/zed-industries/zed/issues/23489



https://github.com/user-attachments/assets/6466e0c1-4188-4980-8bb6-52ef6e7591c9


Release Notes:

- Made remote projects to sync in local user settings

Change summary

crates/project/src/project.rs                    |  9 ++
crates/project/src/project_settings.rs           | 55 ++++++++++++++++-
crates/proto/proto/worktree.proto                |  5 +
crates/proto/proto/zed.proto                     |  4 
crates/proto/src/proto.rs                        |  2 
crates/remote_server/src/remote_editing_tests.rs |  6 +
crates/remote_server/src/unix.rs                 | 48 ++++++++-------
crates/settings/src/settings_store.rs            | 21 ++----
8 files changed, 105 insertions(+), 45 deletions(-)

Detailed changes

crates/project/src/project.rs 🔗

@@ -1271,6 +1271,7 @@ impl Project {
                     fs.clone(),
                     worktree_store.clone(),
                     task_store.clone(),
+                    Some(remote_proto.clone()),
                     cx,
                 )
             });
@@ -1521,7 +1522,13 @@ impl Project {
         })?;
 
         let settings_observer = cx.new(|cx| {
-            SettingsObserver::new_remote(fs.clone(), worktree_store.clone(), task_store.clone(), cx)
+            SettingsObserver::new_remote(
+                fs.clone(),
+                worktree_store.clone(),
+                task_store.clone(),
+                None,
+                cx,
+            )
         })?;
 
         let git_store = cx.new(|cx| {

crates/project/src/project_settings.rs 🔗

@@ -4,7 +4,7 @@ use context_server::ContextServerCommand;
 use dap::adapters::DebugAdapterName;
 use fs::Fs;
 use futures::StreamExt as _;
-use gpui::{App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Task};
+use gpui::{App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Subscription, Task};
 use lsp::LanguageServerName;
 use paths::{
     EDITORCONFIG_NAME, local_debug_file_relative_path, local_settings_file_relative_path,
@@ -13,7 +13,7 @@ use paths::{
 };
 use rpc::{
     AnyProtoClient, TypedEnvelope,
-    proto::{self, FromProto, ToProto},
+    proto::{self, FromProto, REMOTE_SERVER_PROJECT_ID, ToProto},
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -658,6 +658,7 @@ pub struct SettingsObserver {
     worktree_store: Entity<WorktreeStore>,
     project_id: u64,
     task_store: Entity<TaskStore>,
+    _user_settings_watcher: Option<Subscription>,
     _global_task_config_watcher: Task<()>,
     _global_debug_config_watcher: Task<()>,
 }
@@ -670,6 +671,7 @@ pub struct SettingsObserver {
 impl SettingsObserver {
     pub fn init(client: &AnyProtoClient) {
         client.add_entity_message_handler(Self::handle_update_worktree_settings);
+        client.add_entity_message_handler(Self::handle_update_user_settings);
     }
 
     pub fn new_local(
@@ -686,7 +688,8 @@ impl SettingsObserver {
             task_store,
             mode: SettingsObserverMode::Local(fs.clone()),
             downstream_client: None,
-            project_id: 0,
+            _user_settings_watcher: None,
+            project_id: REMOTE_SERVER_PROJECT_ID,
             _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
                 fs.clone(),
                 paths::tasks_file().clone(),
@@ -704,14 +707,38 @@ impl SettingsObserver {
         fs: Arc<dyn Fs>,
         worktree_store: Entity<WorktreeStore>,
         task_store: Entity<TaskStore>,
+        upstream_client: Option<AnyProtoClient>,
         cx: &mut Context<Self>,
     ) -> Self {
+        let mut user_settings_watcher = None;
+        if cx.try_global::<SettingsStore>().is_some() {
+            if let Some(upstream_client) = upstream_client {
+                let mut user_settings = None;
+                user_settings_watcher = Some(cx.observe_global::<SettingsStore>(move |_, cx| {
+                    let new_settings = cx.global::<SettingsStore>().raw_user_settings();
+                    if Some(new_settings) != user_settings.as_ref() {
+                        if let Some(new_settings_string) = serde_json::to_string(new_settings).ok()
+                        {
+                            user_settings = Some(new_settings.clone());
+                            upstream_client
+                                .send(proto::UpdateUserSettings {
+                                    project_id: REMOTE_SERVER_PROJECT_ID,
+                                    contents: new_settings_string,
+                                })
+                                .log_err();
+                        }
+                    }
+                }));
+            }
+        };
+
         Self {
             worktree_store,
             task_store,
             mode: SettingsObserverMode::Remote,
             downstream_client: None,
-            project_id: 0,
+            project_id: REMOTE_SERVER_PROJECT_ID,
+            _user_settings_watcher: user_settings_watcher,
             _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
                 fs.clone(),
                 paths::tasks_file().clone(),
@@ -803,6 +830,24 @@ impl SettingsObserver {
         Ok(())
     }
 
+    async fn handle_update_user_settings(
+        _: Entity<Self>,
+        envelope: TypedEnvelope<proto::UpdateUserSettings>,
+        cx: AsyncApp,
+    ) -> anyhow::Result<()> {
+        let new_settings = serde_json::from_str::<serde_json::Value>(&envelope.payload.contents)
+            .with_context(|| {
+                format!("deserializing {} user settings", envelope.payload.contents)
+            })?;
+        cx.update_global(|settings_store: &mut SettingsStore, cx| {
+            settings_store
+                .set_raw_user_settings(new_settings, cx)
+                .context("setting new user settings")?;
+            anyhow::Ok(())
+        })??;
+        Ok(())
+    }
+
     fn on_worktree_store_event(
         &mut self,
         _: Entity<WorktreeStore>,
@@ -1089,7 +1134,7 @@ impl SettingsObserver {
                         project_id: self.project_id,
                         worktree_id: remote_worktree_id.to_proto(),
                         path: directory.to_proto(),
-                        content: file_content,
+                        content: file_content.clone(),
                         kind: Some(local_settings_kind_to_proto(kind).into()),
                     })
                     .log_err();

crates/proto/proto/worktree.proto 🔗

@@ -150,3 +150,8 @@ enum LocalSettingsKind {
     Editorconfig = 2;
     Debug = 3;
 }
+
+message UpdateUserSettings {
+    uint64 project_id = 1;
+    string contents = 2;
+}

crates/proto/proto/zed.proto 🔗

@@ -397,7 +397,9 @@ message Envelope {
 
         LspQuery lsp_query = 365;
         LspQueryResponse lsp_query_response = 366;
-        ToggleLspLogs toggle_lsp_logs = 367; // current max
+        ToggleLspLogs toggle_lsp_logs = 367;
+
+        UpdateUserSettings update_user_settings = 368; // current max
     }
 
     reserved 87 to 88;

crates/proto/src/proto.rs 🔗

@@ -278,6 +278,7 @@ messages!(
     (UpdateUserChannels, Foreground),
     (UpdateWorktree, Foreground),
     (UpdateWorktreeSettings, Foreground),
+    (UpdateUserSettings, Background),
     (UpdateRepository, Foreground),
     (RemoveRepository, Foreground),
     (UsersResponse, Foreground),
@@ -583,6 +584,7 @@ entity_messages!(
     UpdateRepository,
     RemoveRepository,
     UpdateWorktreeSettings,
+    UpdateUserSettings,
     LspExtExpandMacro,
     LspExtOpenDocs,
     LspExtRunnables,

crates/remote_server/src/remote_editing_tests.rs 🔗

@@ -280,7 +280,8 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
             AllLanguageSettings::get_global(cx)
                 .language(None, Some(&"Rust".into()), cx)
                 .language_servers,
-            ["..."] // local settings are ignored
+            ["from-local-settings"],
+            "User language settings should be synchronized with the server settings"
         )
     });
 
@@ -300,7 +301,8 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
             AllLanguageSettings::get_global(cx)
                 .language(None, Some(&"Rust".into()), cx)
                 .language_servers,
-            ["from-server-settings".to_string()]
+            ["from-server-settings".to_string()],
+            "Server language settings should take precedence over the user settings"
         )
     });
 

crates/remote_server/src/unix.rs 🔗

@@ -918,29 +918,33 @@ fn initialize_settings(
     });
 
     let (mut tx, rx) = watch::channel(None);
+    let mut node_settings = None;
     cx.observe_global::<SettingsStore>(move |cx| {
-        let settings = &ProjectSettings::get_global(cx).node;
-        log::info!("Got new node settings: {:?}", settings);
-        let options = NodeBinaryOptions {
-            allow_path_lookup: !settings.ignore_system_version,
-            // TODO: Implement this setting
-            allow_binary_download: true,
-            use_paths: settings.path.as_ref().map(|node_path| {
-                let node_path = PathBuf::from(shellexpand::tilde(node_path).as_ref());
-                let npm_path = settings
-                    .npm_path
-                    .as_ref()
-                    .map(|path| PathBuf::from(shellexpand::tilde(&path).as_ref()));
-                (
-                    node_path.clone(),
-                    npm_path.unwrap_or_else(|| {
-                        let base_path = PathBuf::new();
-                        node_path.parent().unwrap_or(&base_path).join("npm")
-                    }),
-                )
-            }),
-        };
-        tx.send(Some(options)).log_err();
+        let new_node_settings = &ProjectSettings::get_global(cx).node;
+        if Some(new_node_settings) != node_settings.as_ref() {
+            log::info!("Got new node settings: {new_node_settings:?}");
+            let options = NodeBinaryOptions {
+                allow_path_lookup: !new_node_settings.ignore_system_version,
+                // TODO: Implement this setting
+                allow_binary_download: true,
+                use_paths: new_node_settings.path.as_ref().map(|node_path| {
+                    let node_path = PathBuf::from(shellexpand::tilde(node_path).as_ref());
+                    let npm_path = new_node_settings
+                        .npm_path
+                        .as_ref()
+                        .map(|path| PathBuf::from(shellexpand::tilde(&path).as_ref()));
+                    (
+                        node_path.clone(),
+                        npm_path.unwrap_or_else(|| {
+                            let base_path = PathBuf::new();
+                            node_path.parent().unwrap_or(&base_path).join("npm")
+                        }),
+                    )
+                }),
+            };
+            node_settings = Some(new_node_settings.clone());
+            tx.send(Some(options)).ok();
+        }
     })
     .detach();
 

crates/settings/src/settings_store.rs 🔗

@@ -467,6 +467,13 @@ impl SettingsStore {
         &self.raw_user_settings
     }
 
+    /// Replaces current settings with the values from the given JSON.
+    pub fn set_raw_user_settings(&mut self, new_settings: Value, cx: &mut App) -> Result<()> {
+        self.raw_user_settings = new_settings;
+        self.recompute_values(None, cx)?;
+        Ok(())
+    }
+
     /// Get the configured settings profile names.
     pub fn configured_settings_profiles(&self) -> impl Iterator<Item = &str> {
         self.raw_user_settings
@@ -525,20 +532,6 @@ impl SettingsStore {
         }
     }
 
-    pub async fn load_global_settings(fs: &Arc<dyn Fs>) -> Result<String> {
-        match fs.load(paths::global_settings_file()).await {
-            result @ Ok(_) => result,
-            Err(err) => {
-                if let Some(e) = err.downcast_ref::<std::io::Error>()
-                    && e.kind() == std::io::ErrorKind::NotFound
-                {
-                    return Ok("{}".to_string());
-                }
-                Err(err)
-            }
-        }
-    }
-
     fn update_settings_file_inner(
         &self,
         fs: Arc<dyn Fs>,