From f944ebc4cbc6deb55cfbd6b0466631ea8d3e56a7 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 15 Oct 2024 23:32:44 -0700 Subject: [PATCH] Add settings to remote servers, use XDG paths on remote, and enable node LSPs (#19176) Supersedes https://github.com/zed-industries/zed/pull/19166 TODO: - [x] Update basic zed paths - [x] update create_state_directory - [x] Use this with `NodeRuntime` - [x] Add server settings - [x] Add an 'open server settings command' - [x] Make sure it all works Release Notes: - Updated the actions `zed::OpenLocalSettings` and `zed::OpenLocalTasks` to `zed::OpenProjectSettings` and `zed::OpenProjectTasks`. --------- Co-authored-by: Conrad Co-authored-by: Richard --- Cargo.lock | 4 + assets/settings/initial_server_settings.json | 7 + crates/assistant/src/inline_assistant.rs | 2 +- .../assistant/src/slash_command_settings.rs | 5 +- .../src/terminal_inline_assistant.rs | 2 +- crates/auto_update/src/auto_update.rs | 2 +- crates/client/src/client.rs | 19 +- .../remote_editing_collaboration_tests.rs | 24 +- crates/editor/src/git/blame.rs | 13 +- crates/extension/src/extension_settings.rs | 5 +- crates/extensions_ui/src/extension_suggest.rs | 2 +- crates/gpui/src/app/entity_map.rs | 6 + crates/http_client/src/http_client.rs | 6 + crates/language/src/language.rs | 2 +- crates/paths/src/paths.rs | 11 + crates/project/src/lsp_store.rs | 11 +- crates/project/src/project.rs | 124 +++++++--- crates/project/src/project_settings.rs | 67 +++--- crates/project/src/task_store.rs | 7 +- crates/project/src/terminals.rs | 6 +- crates/project/src/worktree_store.rs | 18 +- crates/project_panel/src/project_panel.rs | 7 +- crates/proto/proto/zed.proto | 22 +- crates/proto/src/proto.rs | 10 +- crates/recent_projects/Cargo.toml | 1 + crates/recent_projects/src/dev_servers.rs | 4 +- crates/recent_projects/src/ssh_connections.rs | 16 +- crates/remote/src/ssh_session.rs | 10 +- crates/remote_server/Cargo.toml | 6 +- crates/remote_server/src/headless_project.rs | 91 +++++++- .../remote_server/src/remote_editing_tests.rs | 62 ++++- crates/remote_server/src/remote_server.rs | 2 +- crates/remote_server/src/unix.rs | 219 +++++++++++++++--- crates/settings/src/settings.rs | 6 +- crates/settings/src/settings_store.rs | 86 +++++-- crates/theme/src/settings.rs | 7 +- crates/vim/src/vim.rs | 11 +- crates/welcome/src/base_keymap_setting.rs | 3 + crates/workspace/src/notifications.rs | 27 +-- crates/workspace/src/workspace.rs | 39 ++-- crates/zed/src/zed.rs | 45 +++- crates/zed/src/zed/app_menus.rs | 2 +- crates/zed_actions/src/lib.rs | 1 + docs/src/configuring-zed.md | 2 +- 44 files changed, 804 insertions(+), 218 deletions(-) create mode 100644 assets/settings/initial_server_settings.json diff --git a/Cargo.lock b/Cargo.lock index f83feb4689db3f9be112efbc9117f672923485b2..6f1d7b2c9ca06dc1ee0629732a437e51fe3e859c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8977,6 +8977,7 @@ dependencies = [ "log", "menu", "ordered-float 2.10.1", + "paths", "picker", "project", "release_channel", @@ -9136,6 +9137,7 @@ name = "remote_server" version = "0.1.0" dependencies = [ "anyhow", + "async-watch", "backtrace", "cargo_toml", "clap", @@ -9151,8 +9153,10 @@ dependencies = [ "log", "lsp", "node_runtime", + "paths", "project", "remote", + "reqwest_client", "rpc", "rust-embed", "serde", diff --git a/assets/settings/initial_server_settings.json b/assets/settings/initial_server_settings.json new file mode 100644 index 0000000000000000000000000000000000000000..d6ec33e60128380378610a273a1bbdff1ecdbaa8 --- /dev/null +++ b/assets/settings/initial_server_settings.json @@ -0,0 +1,7 @@ +// Server-specific settings +// +// For a full list of overridable settings, and general information on settings, +// see the documentation: https://zed.dev/docs/configuring-zed#settings-files +{ + "lsp": {} +} diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 4ecf8dd37e5e08534ad4380eb124e2dd7cbcaf3e..ce01e63b513db0f48bad56b9edeed9bfb7a9ace1 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -2278,7 +2278,7 @@ impl InlineAssist { struct InlineAssistantError; let id = - NotificationId::identified::( + NotificationId::composite::( assist_id.0, ); diff --git a/crates/assistant/src/slash_command_settings.rs b/crates/assistant/src/slash_command_settings.rs index c524b37803edea2ccaffe062021d455ba0c30857..5918769d711c3f96310bfa6e1e13e30fbe0a9a1b 100644 --- a/crates/assistant/src/slash_command_settings.rs +++ b/crates/assistant/src/slash_command_settings.rs @@ -38,7 +38,10 @@ impl Settings for SlashCommandSettings { fn load(sources: SettingsSources, _cx: &mut AppContext) -> Result { SettingsSources::::json_merge_with( - [sources.default].into_iter().chain(sources.user), + [sources.default] + .into_iter() + .chain(sources.user) + .chain(sources.server), ) } } diff --git a/crates/assistant/src/terminal_inline_assistant.rs b/crates/assistant/src/terminal_inline_assistant.rs index d30ec2df11a793935edd019256a8fbbab0f45d7d..41b8d9eb88ac250c698c65c4a19fcab98e1fd593 100644 --- a/crates/assistant/src/terminal_inline_assistant.rs +++ b/crates/assistant/src/terminal_inline_assistant.rs @@ -414,7 +414,7 @@ impl TerminalInlineAssist { struct InlineAssistantError; let id = - NotificationId::identified::( + NotificationId::composite::( assist_id.0, ); diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index bb952990fccc8789ef51b6bebd916bbdb203a272..d501e6d93fdc11b0af89f8ec8010d7a8d97bde81 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -130,7 +130,7 @@ impl Settings for AutoUpdateSetting { type FileContent = Option; fn load(sources: SettingsSources, _: &mut AppContext) -> Result { - let auto_update = [sources.release_channel, sources.user] + let auto_update = [sources.server, sources.release_channel, sources.user] .into_iter() .find_map(|value| value.copied().flatten()) .unwrap_or(sources.default.ok_or_else(Self::missing_default)?); diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 49f3ff4b146f4b89ef4a42b1f8a4c9797a0c8221..33e2bff6fbc5088bdd7a6d71e9828a44b7751ffa 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -141,6 +141,7 @@ impl Settings for ProxySettings { Ok(Self { proxy: sources .user + .or(sources.server) .and_then(|value| value.proxy.clone()) .or(sources.default.proxy.clone()), }) @@ -472,15 +473,21 @@ impl settings::Settings for TelemetrySettings { fn load(sources: SettingsSources, _: &mut AppContext) -> Result { Ok(Self { - diagnostics: sources.user.as_ref().and_then(|v| v.diagnostics).unwrap_or( - sources - .default - .diagnostics - .ok_or_else(Self::missing_default)?, - ), + diagnostics: sources + .user + .as_ref() + .or(sources.server.as_ref()) + .and_then(|v| v.diagnostics) + .unwrap_or( + sources + .default + .diagnostics + .ok_or_else(Self::missing_default)?, + ), metrics: sources .user .as_ref() + .or(sources.server.as_ref()) .and_then(|v| v.metrics) .unwrap_or(sources.default.metrics.ok_or_else(Self::missing_default)?), }) diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index 7de50511ea27662139dd35c69ce16892e319f367..dae33457555ec4dab82288fcca5884be4292ddc0 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -2,10 +2,12 @@ use crate::tests::TestServer; use call::ActiveCall; use fs::{FakeFs, Fs as _}; use gpui::{Context as _, TestAppContext}; -use language::language_settings::all_language_settings; +use http_client::BlockedHttpClient; +use language::{language_settings::all_language_settings, LanguageRegistry}; +use node_runtime::NodeRuntime; use project::ProjectPath; use remote::SshRemoteClient; -use remote_server::HeadlessProject; +use remote_server::{HeadlessAppState, HeadlessProject}; use serde_json::json; use std::{path::Path, sync::Arc}; @@ -48,8 +50,22 @@ async fn test_sharing_an_ssh_remote_project( // User A connects to the remote project via SSH. server_cx.update(HeadlessProject::init); - let _headless_project = - server_cx.new_model(|cx| HeadlessProject::new(server_ssh, remote_fs.clone(), cx)); + let remote_http_client = Arc::new(BlockedHttpClient); + let node = NodeRuntime::unavailable(); + let languages = Arc::new(LanguageRegistry::new(server_cx.executor())); + let _headless_project = server_cx.new_model(|cx| { + client::init_settings(cx); + HeadlessProject::new( + HeadlessAppState { + session: server_ssh, + fs: remote_fs.clone(), + http_client: remote_http_client, + node_runtime: node, + languages, + }, + cx, + ) + }); let (project_a, worktree_id) = client_a .build_ssh_project("/code/project1", client_ssh, cx_a) diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index 303ead16b22319463e411a83f73ef7cdd8ffeb5c..1ac134530532c0052652d5067963371360552593 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -403,7 +403,10 @@ impl GitBlame { if this.user_triggered { log::error!("failed to get git blame data: {error:?}"); let notification = format!("{:#}", error).trim().to_string(); - cx.emit(project::Event::Notification(notification)); + cx.emit(project::Event::Toast { + notification_id: "git-blame".into(), + message: notification, + }); } else { // If we weren't triggered by a user, we just log errors in the background, instead of sending // notifications. @@ -619,9 +622,11 @@ mod tests { let event = project.next_event(cx).await; assert_eq!( event, - project::Event::Notification( - "Failed to blame \"file.txt\": failed to get blame for \"file.txt\"".to_string() - ) + project::Event::Toast { + notification_id: "git-blame".into(), + message: "Failed to blame \"file.txt\": failed to get blame for \"file.txt\"" + .to_string() + } ); blame.update(cx, |blame, cx| { diff --git a/crates/extension/src/extension_settings.rs b/crates/extension/src/extension_settings.rs index a2ab7ac9cca73b1b2ecddd05df786c301228f2c7..cfae4482c90ecac7dd0ef49e84e0ee9d9b459aa7 100644 --- a/crates/extension/src/extension_settings.rs +++ b/crates/extension/src/extension_settings.rs @@ -42,7 +42,10 @@ impl Settings for ExtensionSettings { fn load(sources: SettingsSources, _cx: &mut AppContext) -> Result { SettingsSources::::json_merge_with( - [sources.default].into_iter().chain(sources.user), + [sources.default] + .into_iter() + .chain(sources.user) + .chain(sources.server), ) } } diff --git a/crates/extensions_ui/src/extension_suggest.rs b/crates/extensions_ui/src/extension_suggest.rs index ed003f25b7f2353f4c1d371127f4d77e03ad3400..b21621537fdf12045bcb801172c9e2ab78a622c7 100644 --- a/crates/extensions_ui/src/extension_suggest.rs +++ b/crates/extensions_ui/src/extension_suggest.rs @@ -163,7 +163,7 @@ pub(crate) fn suggest(buffer: Model, cx: &mut ViewContext) { struct ExtensionSuggestionNotification; - let notification_id = NotificationId::identified::( + let notification_id = NotificationId::composite::( SharedString::from(extension_id.clone()), ); diff --git a/crates/gpui/src/app/entity_map.rs b/crates/gpui/src/app/entity_map.rs index e4da2a3010ae356807b13308d39d1b8757b72ae7..5f65cebdb2828ef4564b1ddb88b930671de26afe 100644 --- a/crates/gpui/src/app/entity_map.rs +++ b/crates/gpui/src/app/entity_map.rs @@ -567,6 +567,12 @@ pub struct WeakModel { entity_type: PhantomData, } +impl std::fmt::Debug for WeakModel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct(type_name::>()).finish() + } +} + unsafe impl Send for WeakModel {} unsafe impl Sync for WeakModel {} diff --git a/crates/http_client/src/http_client.rs b/crates/http_client/src/http_client.rs index 3d4a41f4a61df140952a873a3501673d70ca853c..3aadf7496f5f678682bbfda9c5af2752cd01effe 100644 --- a/crates/http_client/src/http_client.rs +++ b/crates/http_client/src/http_client.rs @@ -317,6 +317,12 @@ pub fn read_proxy_from_env() -> Option { pub struct BlockedHttpClient; +impl BlockedHttpClient { + pub fn new() -> Self { + BlockedHttpClient + } +} + impl HttpClient for BlockedHttpClient { fn send( &self, diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index fad799da19898d1d0f1b05b4e0e316a5fc469078..c1c9cfebbead5ebd0a51e77259dd699fd7dab13b 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -367,7 +367,7 @@ pub trait LspAdapter: 'static + Send + Sync { } let Some(container_dir) = delegate.language_server_download_dir(&self.name()).await else { - anyhow::bail!("cannot download language servers for remotes (yet)") + anyhow::bail!("no language server download dir defined") }; let mut binary = try_fetch_server_binary(self.as_ref(), &delegate, container_dir.to_path_buf(), cx).await; diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index 7f662d0325d1bf04f676500dfcd6e50431a7c0b0..1649ccb475de92199b7426e59d0e862a9b5cb1fa 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -5,6 +5,11 @@ use std::sync::OnceLock; pub use util::paths::home_dir; +/// Returns the relative path to the zed_server directory on the ssh host. +pub fn remote_server_dir_relative() -> &'static Path { + Path::new(".zed_server") +} + /// Returns the path to the configuration directory used by Zed. pub fn config_dir() -> &'static PathBuf { static CONFIG_DIR: OnceLock = OnceLock::new(); @@ -96,6 +101,12 @@ pub fn logs_dir() -> &'static PathBuf { }) } +/// Returns the path to the zed server directory on this ssh host. +pub fn remote_server_state_dir() -> &'static PathBuf { + static REMOTE_SERVER_STATE: OnceLock = OnceLock::new(); + REMOTE_SERVER_STATE.get_or_init(|| return support_dir().join("server_state")) +} + /// Returns the path to the `Zed.log` file. pub fn log_file() -> &'static PathBuf { static LOG_FILE: OnceLock = OnceLock::new(); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 58d6b07e565c42dcce0747f4889bc334bdb261df..6b0042638dc7a1a96f24af92510fd72571bb027d 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -27,7 +27,7 @@ use gpui::{ AppContext, AsyncAppContext, Context, Entity, EventEmitter, Model, ModelContext, PromptLevel, Task, WeakModel, }; -use http_client::{BlockedHttpClient, HttpClient}; +use http_client::HttpClient; use language::{ language_settings::{ all_language_settings, language_settings, AllLanguageSettings, FormatOnSave, Formatter, @@ -116,7 +116,7 @@ impl FormatTrigger { } pub struct LocalLspStore { - http_client: Option>, + http_client: Arc, environment: Model, fs: Arc, yarn: Model, @@ -839,7 +839,7 @@ impl LspStore { prettier_store: Model, environment: Model, languages: Arc, - http_client: Option>, + http_client: Arc, fs: Arc, cx: &mut ModelContext, ) -> Self { @@ -7579,10 +7579,7 @@ impl LocalLspAdapterDelegate { .as_local() .expect("LocalLspAdapterDelegate cannot be constructed on a remote"); - let http_client = local - .http_client - .clone() - .unwrap_or_else(|| Arc::new(BlockedHttpClient)); + let http_client = local.http_client.clone(); Self::new(lsp_store, worktree, http_client, local.fs.clone(), cx) } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f4040a3d12c6e764d147ac48b56998e90fb30795..6e731e9cbadd014750ea3eef709b800b8af9c585 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -222,8 +222,13 @@ pub enum Event { LanguageServerAdded(LanguageServerId, LanguageServerName, Option), LanguageServerRemoved(LanguageServerId), LanguageServerLog(LanguageServerId, LanguageServerLogType, String), - Notification(String), - LocalSettingsUpdated(Result<(), InvalidSettingsError>), + Toast { + notification_id: SharedString, + message: String, + }, + HideToast { + notification_id: SharedString, + }, LanguageServerPrompt(LanguageServerPromptRequest), LanguageNotFound(Model), ActiveEntryChanged(Option), @@ -633,7 +638,7 @@ impl Project { prettier_store.clone(), environment.clone(), languages.clone(), - Some(client.http_client()), + client.http_client(), fs.clone(), cx, ) @@ -694,7 +699,7 @@ impl Project { let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx); - let ssh_proto = ssh.read(cx).to_proto_client(); + let ssh_proto = ssh.read(cx).proto_client(); let worktree_store = cx.new_model(|_| WorktreeStore::remote(false, ssh_proto.clone(), 0, None)); cx.subscribe(&worktree_store, Self::on_worktree_store_event) @@ -703,7 +708,7 @@ impl Project { let buffer_store = cx.new_model(|cx| { BufferStore::remote( worktree_store.clone(), - ssh.read(cx).to_proto_client(), + ssh.read(cx).proto_client(), SSH_PROJECT_ID, cx, ) @@ -716,7 +721,7 @@ impl Project { fs.clone(), buffer_store.downgrade(), worktree_store.clone(), - ssh.read(cx).to_proto_client(), + ssh.read(cx).proto_client(), SSH_PROJECT_ID, cx, ) @@ -809,6 +814,8 @@ impl Project { ssh_proto.add_model_message_handler(Self::handle_create_buffer_for_peer); ssh_proto.add_model_message_handler(Self::handle_update_worktree); ssh_proto.add_model_message_handler(Self::handle_update_project); + ssh_proto.add_model_message_handler(Self::handle_toast); + ssh_proto.add_model_message_handler(Self::handle_hide_toast); ssh_proto.add_model_request_handler(BufferStore::handle_update_buffer); BufferStore::init(&ssh_proto); LspStore::init(&ssh_proto); @@ -2065,7 +2072,7 @@ impl Project { if let Some(ref ssh_client) = self.ssh_client { ssh_client .read(cx) - .to_proto_client() + .proto_client() .send(proto::CloseBuffer { project_id: 0, buffer_id: buffer_id.to_proto(), @@ -2136,7 +2143,10 @@ impl Project { .ok(); } } - LspStoreEvent::Notification(message) => cx.emit(Event::Notification(message.clone())), + LspStoreEvent::Notification(message) => cx.emit(Event::Toast { + notification_id: "lsp".into(), + message: message.clone(), + }), LspStoreEvent::SnippetEdit { buffer_id, edits, @@ -2180,9 +2190,20 @@ impl Project { cx: &mut ModelContext, ) { match event { - SettingsObserverEvent::LocalSettingsUpdated(error) => { - cx.emit(Event::LocalSettingsUpdated(error.clone())) - } + SettingsObserverEvent::LocalSettingsUpdated(result) => match result { + Err(InvalidSettingsError::LocalSettings { message, path }) => { + let message = + format!("Failed to set local settings in {:?}:\n{}", path, message); + cx.emit(Event::Toast { + notification_id: "local-settings".into(), + message, + }); + } + Ok(_) => cx.emit(Event::HideToast { + notification_id: "local-settings".into(), + }), + Err(_) => {} + }, } } @@ -2262,7 +2283,7 @@ impl Project { if let Some(ssh) = &self.ssh_client { ssh.read(cx) - .to_proto_client() + .proto_client() .send(proto::RemoveWorktree { worktree_id: id_to_remove.to_proto(), }) @@ -2295,7 +2316,7 @@ impl Project { if let Some(ssh) = &self.ssh_client { ssh.read(cx) - .to_proto_client() + .proto_client() .send(proto::UpdateBuffer { project_id: 0, buffer_id: buffer_id.to_proto(), @@ -2632,6 +2653,35 @@ impl Project { }) } + pub fn open_server_settings( + &mut self, + cx: &mut ModelContext, + ) -> Task>> { + let guard = self.retain_remotely_created_models(cx); + let Some(ssh_client) = self.ssh_client.as_ref() else { + return Task::ready(Err(anyhow!("not an ssh project"))); + }; + + let proto_client = ssh_client.read(cx).proto_client(); + + cx.spawn(|this, mut cx| async move { + let buffer = proto_client + .request(proto::OpenServerSettings { + project_id: SSH_PROJECT_ID, + }) + .await?; + + let buffer = this + .update(&mut cx, |this, cx| { + anyhow::Ok(this.wait_for_remote_buffer(BufferId::new(buffer.buffer_id)?, cx)) + })?? + .await; + + drop(guard); + buffer + }) + } + pub fn open_local_buffer_via_lsp( &mut self, abs_path: lsp::Url, @@ -2982,7 +3032,7 @@ impl Project { let (tx, rx) = smol::channel::unbounded(); let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.ssh_client { - (ssh_client.read(cx).to_proto_client(), 0) + (ssh_client.read(cx).proto_client(), 0) } else if let Some(remote_id) = self.remote_id() { (self.client.clone().into(), remote_id) } else { @@ -3069,14 +3119,9 @@ impl Project { visible: bool, cx: &mut ModelContext, ) -> Task, PathBuf)>> { - let abs_path = abs_path.as_ref(); - if let Some((tree, relative_path)) = self.find_worktree(abs_path, cx) { - Task::ready(Ok((tree, relative_path))) - } else { - let worktree = self.create_worktree(abs_path, visible, cx); - cx.background_executor() - .spawn(async move { Ok((worktree.await?, PathBuf::new())) }) - } + self.worktree_store.update(cx, |worktree_store, cx| { + worktree_store.find_or_create_worktree(abs_path, visible, cx) + }) } pub fn find_worktree( @@ -3138,7 +3183,7 @@ impl Project { } else if let Some(ssh_client) = self.ssh_client.as_ref() { let request = ssh_client .read(cx) - .to_proto_client() + .proto_client() .request(proto::CheckFileExists { project_id: SSH_PROJECT_ID, path: path.to_string(), @@ -3215,7 +3260,7 @@ impl Project { path: query, }; - let response = session.read(cx).to_proto_client().request(request); + let response = session.read(cx).proto_client().request(request); cx.background_executor().spawn(async move { let response = response.await?; Ok(response.entries.into_iter().map(PathBuf::from).collect()) @@ -3239,7 +3284,7 @@ impl Project { } } - fn create_worktree( + pub fn create_worktree( &mut self, abs_path: impl AsRef, visible: bool, @@ -3544,6 +3589,33 @@ impl Project { })? } + async fn handle_toast( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |_, cx| { + cx.emit(Event::Toast { + notification_id: envelope.payload.notification_id.into(), + message: envelope.payload.message, + }); + Ok(()) + })? + } + + async fn handle_hide_toast( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |_, cx| { + cx.emit(Event::HideToast { + notification_id: envelope.payload.notification_id.into(), + }); + Ok(()) + })? + } + // Collab sends UpdateWorktree protos as messages async fn handle_update_worktree( this: Model, @@ -3572,7 +3644,7 @@ impl Project { let mut payload = envelope.payload.clone(); payload.project_id = SSH_PROJECT_ID; cx.background_executor() - .spawn(ssh.read(cx).to_proto_client().request(payload)) + .spawn(ssh.read(cx).proto_client().request(payload)) .detach_and_log_err(cx); } this.buffer_store.clone() diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 49378fbc1dc7e47fc21a3eea189e41527efcee00..3ecf0e300878caf9eeb45a0c2c9e60102c8f6f62 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -538,26 +538,47 @@ impl SettingsObserver { let task_store = self.task_store.clone(); for (directory, kind, file_content) in settings_contents { - let result = match kind { + match kind { LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx - .update_global::>(|store, cx| { - store.set_local_settings( + .update_global::(|store, cx| { + let result = store.set_local_settings( worktree_id, directory.clone(), kind, file_content.as_deref(), cx, - ) + ); + + match result { + Err(InvalidSettingsError::LocalSettings { path, message }) => { + log::error!( + "Failed to set local settings in {:?}: {:?}", + path, + message + ); + cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err( + InvalidSettingsError::LocalSettings { path, message }, + ))); + } + Err(e) => { + log::error!("Failed to set local settings: {e}"); + } + Ok(_) => { + cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(()))); + } + } }), LocalSettingsKind::Tasks => task_store.update(cx, |task_store, cx| { - task_store.update_user_tasks( - Some(SettingsLocation { - worktree_id, - path: directory.as_ref(), - }), - file_content.as_deref(), - cx, - ) + task_store + .update_user_tasks( + Some(SettingsLocation { + worktree_id, + path: directory.as_ref(), + }), + file_content.as_deref(), + cx, + ) + .log_err(); }), }; @@ -572,28 +593,6 @@ impl SettingsObserver { }) .log_err(); } - - match result { - Err(error) => { - if let Ok(error) = error.downcast::() { - if let InvalidSettingsError::LocalSettings { - ref path, - ref message, - } = error - { - log::error!( - "Failed to set local settings in {:?}: {:?}", - path, - message - ); - cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(error))); - } - } - } - Ok(()) => { - cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(()))); - } - } } } } diff --git a/crates/project/src/task_store.rs b/crates/project/src/task_store.rs index 55ee780fc56e98ef302e3364bc2cecf1c98d0f88..45b62697c3df37548ed9dce7f0054edfff2f5645 100644 --- a/crates/project/src/task_store.rs +++ b/crates/project/src/task_store.rs @@ -298,9 +298,10 @@ impl TaskStore { let result = task_store.update_user_tasks(None, Some(&user_tasks_content), cx); if let Err(err) = &result { log::error!("Failed to load user tasks: {err}"); - cx.emit(crate::Event::Notification(format!( - "Invalid global tasks file\n{err}" - ))); + cx.emit(crate::Event::Toast { + notification_id: "load-user-tasks".into(), + message: format!("Invalid global tasks file\n{err}"), + }); } cx.refresh(); }) else { diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 11b3152f0cf66070f3f0735d45c61750c5e6bcbc..ac4e890aaddfa68ec60e94167e55485a561c76c2 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -367,7 +367,11 @@ pub fn wrap_for_ssh( // replace ith with something that works let tilde_prefix = "~/"; if path.starts_with(tilde_prefix) { - let trimmed_path = &path_string[tilde_prefix.len()..]; + let trimmed_path = path_string + .trim_start_matches("/") + .trim_start_matches("~") + .trim_start_matches("/"); + format!("cd \"$HOME/{trimmed_path}\"; {env_changes} {to_run}") } else { format!("cd {path:?}; {env_changes} {to_run}") diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index 6e04a884b9187729fe3d4cef124a011d61532882..55eb0108e2109b9720e8b36c7d167a5cb9e2d77e 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -153,6 +153,22 @@ impl WorktreeStore { None } + pub fn find_or_create_worktree( + &mut self, + abs_path: impl AsRef, + visible: bool, + cx: &mut ModelContext, + ) -> Task, PathBuf)>> { + let abs_path = abs_path.as_ref(); + if let Some((tree, relative_path)) = self.find_worktree(abs_path, cx) { + Task::ready(Ok((tree, relative_path))) + } else { + let worktree = self.create_worktree(abs_path, visible, cx); + cx.background_executor() + .spawn(async move { Ok((worktree.await?, PathBuf::new())) }) + } + } + pub fn entry_for_id<'a>( &'a self, entry_id: ProjectEntryId, @@ -957,7 +973,7 @@ impl WorktreeStore { } } -#[derive(Clone)] +#[derive(Clone, Debug)] enum WorktreeHandle { Strong(Model), Weak(WeakModel), diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index b4f07c66d306555a82332d9ccb2270c470debde6..08a0ef4b4060b41b9a54b8d4895f2e546d64963e 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -880,9 +880,10 @@ impl ProjectPanel { if is_dir { project_panel.project.update(cx, |_, cx| { - cx.emit(project::Event::Notification(format!( - "Created an excluded directory at {abs_path:?}.\nAlter `file_scan_exclusions` in the settings to show it in the panel" - ))) + cx.emit(project::Event::Toast { + notification_id: "excluded-directory".into(), + message: format!("Created an excluded directory at {abs_path:?}.\nAlter `file_scan_exclusions` in the settings to show it in the panel") + }) }); None } else { diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 52a73cd7f1dc8bfe3e4b6e6cca4040eed2dc2666..fbd1cf421513484ebcf536067e155b43e327d0b1 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -287,7 +287,12 @@ message Envelope { RemoveWorktree remove_worktree = 258; - LanguageServerLog language_server_log = 260; // current max + LanguageServerLog language_server_log = 260; + + Toast toast = 261; + HideToast hide_toast = 262; + + OpenServerSettings open_server_settings = 263; // current max } reserved 87 to 88; @@ -2487,3 +2492,18 @@ message ShutdownRemoteServer {} message RemoveWorktree { uint64 worktree_id = 1; } + +message Toast { + uint64 project_id = 1; + string notification_id = 2; + string message = 3; +} + +message HideToast { + uint64 project_id = 1; + string notification_id = 2; +} + +message OpenServerSettings { + uint64 project_id = 1; +} diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 15841a70770922f6a2fdf05c31c1e23aef9c4fdd..8455439980165365d9053dd8c2c19a7f68c2252c 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -367,6 +367,9 @@ messages!( (ShutdownRemoteServer, Foreground), (RemoveWorktree, Foreground), (LanguageServerLog, Foreground), + (Toast, Background), + (HideToast, Background), + (OpenServerSettings, Foreground), ); request_messages!( @@ -490,7 +493,8 @@ request_messages!( (AddWorktree, AddWorktreeResponse), (CheckFileExists, CheckFileExistsResponse), (ShutdownRemoteServer, Ack), - (RemoveWorktree, Ack) + (RemoveWorktree, Ack), + (OpenServerSettings, OpenBufferResponse) ); entity_messages!( @@ -564,6 +568,10 @@ entity_messages!( UpdateUserSettings, CheckFileExists, LanguageServerLog, + Toast, + HideToast, + OpenServerSettings, + ); entity_messages!( diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index 2b428009c4ae936ca08b4aee8194c95aae2413ab..3dadcbef37509d1f3ffc60adfee9cd1b7499b45a 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -39,6 +39,7 @@ terminal_view.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true +paths.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/recent_projects/src/dev_servers.rs b/crates/recent_projects/src/dev_servers.rs index 8fe501258a0bf90d2a1cbab48f6532d8ed72fcc4..89772a1fe81f95aa6c5529b348a6dcb86d9f87c5 100644 --- a/crates/recent_projects/src/dev_servers.rs +++ b/crates/recent_projects/src/dev_servers.rs @@ -948,7 +948,7 @@ impl DevServerProjects { this.show_toast( Toast::new( - NotificationId::identified::< + NotificationId::composite::< SshServerAddressCopiedToClipboard, >( connection_string.clone() @@ -1002,7 +1002,7 @@ impl DevServerProjects { ); this.show_toast( Toast::new( - NotificationId::identified::( + NotificationId::composite::( connection_string.clone(), ), notification, diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index d3752a7b123f92c47ced7a2c603c67eba3ce4efc..06fb08b3fe493d435f9255a344581e8a278edcff 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -10,6 +10,7 @@ use gpui::{ Transformation, View, }; use gpui::{AppContext, Model}; + use release_channel::{AppVersion, ReleaseChannel}; use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient}; use schemars::JsonSchema; @@ -377,9 +378,18 @@ impl remote::SshClientDelegate for SshClientDelegate { rx } - fn remote_server_binary_path(&self, cx: &mut AsyncAppContext) -> Result { + fn remote_server_binary_path( + &self, + platform: SshPlatform, + cx: &mut AsyncAppContext, + ) -> Result { let release_channel = cx.update(|cx| ReleaseChannel::global(cx))?; - Ok(format!(".local/zed-remote-server-{}", release_channel.dev_name()).into()) + Ok(paths::remote_server_dir_relative().join(format!( + "zed-remote-server-{}-{}-{}", + release_channel.dev_name(), + platform.os, + platform.arch + ))) } } @@ -487,7 +497,7 @@ impl SshClientDelegate { let path = std::env::current_dir()?.join("target/remote_server/debug/remote_server.gz"); return Ok(Some((path, version))); } else if let Some(triple) = platform.triple() { - smol::fs::create_dir_all("target/remote-server").await?; + smol::fs::create_dir_all("target/remote_server").await?; self.update_status(Some("Installing cross.rs for cross-compilation"), cx); log::info!("installing cross"); diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index fbe6e3e0fa89a7ca83bde60b752653f32732fbe6..4d131425492da8c3e3f36d762f0ad4ef7593a12a 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -137,7 +137,11 @@ pub trait SshClientDelegate: Send + Sync { prompt: String, cx: &mut AsyncAppContext, ) -> oneshot::Receiver>; - fn remote_server_binary_path(&self, cx: &mut AsyncAppContext) -> Result; + fn remote_server_binary_path( + &self, + platform: SshPlatform, + cx: &mut AsyncAppContext, + ) -> Result; fn get_server_binary( &self, platform: SshPlatform, @@ -972,7 +976,7 @@ impl SshRemoteClient { let platform = ssh_connection.query_platform().await?; let (local_binary_path, version) = delegate.get_server_binary(platform, cx).await??; - let remote_binary_path = delegate.remote_server_binary_path(cx)?; + let remote_binary_path = delegate.remote_server_binary_path(platform, cx)?; ssh_connection .ensure_server_binary( &delegate, @@ -1021,7 +1025,7 @@ impl SshRemoteClient { .map(|ssh_connection| ssh_connection.socket.ssh_args()) } - pub fn to_proto_client(&self) -> AnyProtoClient { + pub fn proto_client(&self) -> AnyProtoClient { self.client.clone().into() } diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index d822721c1eb8a20f2fea3e0b08db23ee23f763b3..6ba2c5c7c97ca736b8879f1fed2d1ff948a425f0 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -22,6 +22,7 @@ debug-embed = ["dep:rust-embed"] test-support = ["fs/test-support"] [dependencies] +async-watch.workspace = true anyhow.workspace = true backtrace = "0.3" clap.workspace = true @@ -30,13 +31,16 @@ env_logger.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true +http_client.workspace = true language.workspace = true languages.workspace = true log.workspace = true lsp.workspace = true node_runtime.workspace = true project.workspace = true +paths = { workspace = true } remote.workspace = true +reqwest_client.workspace = true rpc.workspace = true rust-embed = { workspace = true, optional = true, features = ["debug-embed"] } serde.workspace = true @@ -66,4 +70,4 @@ cargo_toml.workspace = true toml.workspace = true [package.metadata.cargo-machete] -ignored = ["rust-embed"] +ignored = ["rust-embed", "paths"] diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 701b80842b8cebd1a396845b8e52df4a9e8cafac..f17c27a03cad43bdd5bef3f8985b19464b05e78c 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Result}; use fs::Fs; use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext}; +use http_client::HttpClient; use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry}; use node_runtime::NodeRuntime; use project::{ @@ -16,6 +17,8 @@ use rpc::{ proto::{self, SSH_PEER_ID, SSH_PROJECT_ID}, AnyProtoClient, TypedEnvelope, }; + +use settings::initial_server_settings_content; use smol::stream::StreamExt; use std::{ path::{Path, PathBuf}, @@ -36,6 +39,14 @@ pub struct HeadlessProject { pub languages: Arc, } +pub struct HeadlessAppState { + pub session: Arc, + pub fs: Arc, + pub http_client: Arc, + pub node_runtime: NodeRuntime, + pub languages: Arc, +} + impl HeadlessProject { pub fn init(cx: &mut AppContext) { settings::init(cx); @@ -43,11 +54,16 @@ impl HeadlessProject { project::Project::init_settings(cx); } - pub fn new(session: Arc, fs: Arc, cx: &mut ModelContext) -> Self { - let languages = Arc::new(LanguageRegistry::new(cx.background_executor().clone())); - - let node_runtime = NodeRuntime::unavailable(); - + pub fn new( + HeadlessAppState { + session, + fs, + http_client, + node_runtime, + languages, + }: HeadlessAppState, + cx: &mut ModelContext, + ) -> Self { languages::init(languages.clone(), node_runtime.clone(), cx); let worktree_store = cx.new_model(|cx| { @@ -99,7 +115,7 @@ impl HeadlessProject { prettier_store.clone(), environment, languages.clone(), - None, + http_client, fs.clone(), cx, ); @@ -139,6 +155,7 @@ impl HeadlessProject { client.add_model_request_handler(Self::handle_open_buffer_by_path); client.add_model_request_handler(Self::handle_find_search_candidates); + client.add_model_request_handler(Self::handle_open_server_settings); client.add_model_request_handler(BufferStore::handle_update_buffer); client.add_model_message_handler(BufferStore::handle_close_buffer); @@ -203,6 +220,15 @@ impl HeadlessProject { }) .log_err(); } + LspStoreEvent::Notification(message) => { + self.session + .send(proto::Toast { + project_id: SSH_PROJECT_ID, + notification_id: "lsp".to_string(), + message: message.clone(), + }) + .log_err(); + } LspStoreEvent::LanguageServerLog(language_server_id, log_type, message) => { self.session .send(proto::LanguageServerLog { @@ -336,6 +362,59 @@ impl HeadlessProject { }) } + pub async fn handle_open_server_settings( + this: Model, + _: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result { + let settings_path = paths::settings_file(); + let (worktree, path) = this + .update(&mut cx, |this, cx| { + this.worktree_store.update(cx, |worktree_store, cx| { + worktree_store.find_or_create_worktree(settings_path, false, cx) + }) + })? + .await?; + + let (buffer, buffer_store) = this.update(&mut cx, |this, cx| { + let buffer = this.buffer_store.update(cx, |buffer_store, cx| { + buffer_store.open_buffer( + ProjectPath { + worktree_id: worktree.read(cx).id(), + path: path.into(), + }, + cx, + ) + }); + + (buffer, this.buffer_store.clone()) + })?; + + let buffer = buffer.await?; + + let buffer_id = cx.update(|cx| { + if buffer.read(cx).is_empty() { + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, initial_server_settings_content())], None, cx) + }); + } + + let buffer_id = buffer.read_with(cx, |b, _| b.remote_id()); + + buffer_store.update(cx, |buffer_store, cx| { + buffer_store + .create_buffer_for_peer(&buffer, SSH_PEER_ID, cx) + .detach_and_log_err(cx); + }); + + buffer_id + })?; + + Ok(proto::OpenBufferResponse { + buffer_id: buffer_id.to_proto(), + }) + } + pub async fn handle_find_search_candidates( this: Model, envelope: TypedEnvelope, diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 10cfb517abb8027bd56d0b9578e1e26f1b2cf028..41065ad5508310dd071167fc1c043ae65e5857c4 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -3,7 +3,7 @@ use client::{Client, UserStore}; use clock::FakeSystemClock; use fs::{FakeFs, Fs}; use gpui::{Context, Model, TestAppContext}; -use http_client::FakeHttpClient; +use http_client::{BlockedHttpClient, FakeHttpClient}; use language::{ language_settings::{all_language_settings, AllLanguageSettings}, Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LanguageServerName, @@ -17,7 +17,7 @@ use project::{ }; use remote::SshRemoteClient; use serde_json::json; -use settings::{Settings, SettingsLocation, SettingsStore}; +use settings::{initial_server_settings_content, Settings, SettingsLocation, SettingsStore}; use smol::stream::StreamExt; use std::{ path::{Path, PathBuf}, @@ -197,7 +197,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo cx.update_global(|settings_store: &mut SettingsStore, cx| { settings_store.set_user_settings( - r#"{"languages":{"Rust":{"language_servers":["custom-rust-analyzer"]}}}"#, + r#"{"languages":{"Rust":{"language_servers":["from-local-settings"]}}}"#, cx, ) }) @@ -210,7 +210,27 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo AllLanguageSettings::get_global(cx) .language(Some(&"Rust".into())) .language_servers, - ["custom-rust-analyzer".to_string()] + ["from-local-settings".to_string()] + ) + }); + + server_cx + .update_global(|settings_store: &mut SettingsStore, cx| { + settings_store.set_server_settings( + r#"{"languages":{"Rust":{"language_servers":["from-server-settings"]}}}"#, + cx, + ) + }) + .unwrap(); + + cx.run_until_parked(); + + server_cx.read(|cx| { + assert_eq!( + AllLanguageSettings::get_global(cx) + .language(Some(&"Rust".into())) + .language_servers, + ["from-server-settings".to_string()] ) }); @@ -606,6 +626,21 @@ async fn test_adding_then_removing_then_adding_worktrees( }) } +#[gpui::test] +async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { + let (project, _headless, _fs) = init_test(cx, server_cx).await; + let buffer = project.update(cx, |project, cx| project.open_server_settings(cx)); + cx.executor().run_until_parked(); + let buffer = buffer.await.unwrap(); + + cx.update(|cx| { + assert_eq!( + buffer.read(cx).text(), + initial_server_settings_content().to_string() + ) + }) +} + fn init_logger() { if std::env::var("RUST_LOG").is_ok() { env_logger::try_init().ok(); @@ -642,8 +677,23 @@ async fn init_test( ); server_cx.update(HeadlessProject::init); - let headless = - server_cx.new_model(|cx| HeadlessProject::new(ssh_server_client, fs.clone(), cx)); + let http_client = Arc::new(BlockedHttpClient); + let node_runtime = NodeRuntime::unavailable(); + let languages = Arc::new(LanguageRegistry::new(cx.executor())); + let headless = server_cx.new_model(|cx| { + client::init_settings(cx); + + HeadlessProject::new( + crate::HeadlessAppState { + session: ssh_server_client, + fs: fs.clone(), + http_client, + node_runtime, + languages, + }, + cx, + ) + }); let project = build_project(ssh_remote_client, cx); project diff --git a/crates/remote_server/src/remote_server.rs b/crates/remote_server/src/remote_server.rs index 2321ee1c6eb52a08db80965374b92b538acd04c3..52003969af4dfbed6d96863289b5bca506c9315b 100644 --- a/crates/remote_server/src/remote_server.rs +++ b/crates/remote_server/src/remote_server.rs @@ -6,4 +6,4 @@ pub mod unix; #[cfg(test)] mod remote_editing_tests; -pub use headless_project::HeadlessProject; +pub use headless_project::{HeadlessAppState, HeadlessProject}; diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 2b487803fbe67e35614065327f9a3cd06db029db..2972991b7bc64a0785f3c95d55d4cce2c2daaae9 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -1,27 +1,37 @@ +use crate::headless_project::HeadlessAppState; use crate::HeadlessProject; use anyhow::{anyhow, Context, Result}; -use fs::RealFs; +use client::ProxySettings; +use fs::{Fs, RealFs}; use futures::channel::mpsc; use futures::{select, select_biased, AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt, SinkExt}; -use gpui::{AppContext, Context as _}; +use gpui::{AppContext, Context as _, ModelContext, UpdateGlobal as _}; +use http_client::{read_proxy_from_env, Uri}; +use language::LanguageRegistry; +use node_runtime::{NodeBinaryOptions, NodeRuntime}; +use paths::logs_dir; +use project::project_settings::ProjectSettings; use remote::proxy::ProxyLaunchError; use remote::ssh_session::ChannelClient; use remote::{ json_log::LogRecord, protocol::{read_message, write_message}, }; -use rpc::proto::Envelope; +use reqwest_client::ReqwestClient; +use rpc::proto::{self, Envelope, SSH_PROJECT_ID}; +use settings::{watch_config_file, Settings, SettingsStore}; use smol::channel::{Receiver, Sender}; use smol::io::AsyncReadExt; + use smol::Async; use smol::{net::unix::UnixListener, stream::StreamExt as _}; use std::{ - env, io::Write, mem, path::{Path, PathBuf}, sync::Arc, }; +use util::ResultExt; fn init_logging_proxy() { env_logger::builder() @@ -266,6 +276,22 @@ fn start_server( ChannelClient::new(incoming_rx, outgoing_tx, cx) } +fn init_paths() -> anyhow::Result<()> { + for path in [ + paths::config_dir(), + paths::extensions_dir(), + paths::languages_dir(), + paths::logs_dir(), + paths::temp_dir(), + ] + .iter() + { + std::fs::create_dir_all(path) + .map_err(|e| anyhow!("Could not create directory {:?}: {}", path, e))?; + } + Ok(()) +} + pub fn execute_run( log_file: PathBuf, pid_file: PathBuf, @@ -275,6 +301,7 @@ pub fn execute_run( ) -> Result<()> { let log_rx = init_logging_server(log_file)?; init_panic_hook(); + init_paths()?; log::info!( "starting up. pid_file: {:?}, stdin_socket: {:?}, stdout_socket: {:?}, stderr_socket: {:?}", @@ -297,8 +324,43 @@ pub fn execute_run( log::info!("gpui app started, initializing server"); let session = start_server(listeners, log_rx, cx); + client::init_settings(cx); + let project = cx.new_model(|cx| { - HeadlessProject::new(session, Arc::new(RealFs::new(Default::default(), None)), cx) + let fs = Arc::new(RealFs::new(Default::default(), None)); + let node_settings_rx = initialize_settings(session.clone(), fs.clone(), cx); + + let proxy_url = read_proxy_settings(cx); + + let http_client = Arc::new( + ReqwestClient::proxy_and_user_agent( + proxy_url, + &format!( + "Zed-Server/{} ({}; {})", + env!("CARGO_PKG_VERSION"), + std::env::consts::OS, + std::env::consts::ARCH + ), + ) + .expect("Could not start HTTP client"), + ); + + let node_runtime = NodeRuntime::new(http_client.clone(), node_settings_rx); + + let mut languages = LanguageRegistry::new(cx.background_executor().clone()); + languages.set_language_server_download_dir(paths::languages_dir().clone()); + let languages = Arc::new(languages); + + HeadlessProject::new( + HeadlessAppState { + session, + fs, + http_client, + node_runtime, + languages, + }, + cx, + ) }); mem::forget(project); @@ -318,13 +380,15 @@ struct ServerPaths { impl ServerPaths { fn new(identifier: &str) -> Result { - let project_dir = create_state_directory(identifier)?; + let server_dir = paths::remote_server_state_dir().join(identifier); + std::fs::create_dir_all(&server_dir)?; + std::fs::create_dir_all(&logs_dir())?; - let pid_file = project_dir.join("server.pid"); - let stdin_socket = project_dir.join("stdin.sock"); - let stdout_socket = project_dir.join("stdout.sock"); - let stderr_socket = project_dir.join("stderr.sock"); - let log_file = project_dir.join("server.log"); + let pid_file = server_dir.join("server.pid"); + let stdin_socket = server_dir.join("stdin.sock"); + let stdout_socket = server_dir.join("stdout.sock"); + let stderr_socket = server_dir.join("stderr.sock"); + let log_file = logs_dir().join(format!("server-{}.log", identifier)); Ok(Self { pid_file, @@ -358,7 +422,7 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> { } spawn_server(&server_paths)?; - } + }; let stdin_task = smol::spawn(async move { let stdin = Async::new(std::io::stdin())?; @@ -409,19 +473,6 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> { Ok(()) } -fn create_state_directory(identifier: &str) -> Result { - let home_dir = env::var("HOME").unwrap_or_else(|_| ".".to_string()); - let server_dir = PathBuf::from(home_dir) - .join(".local") - .join("state") - .join("zed-remote-server") - .join(identifier); - - std::fs::create_dir_all(&server_dir)?; - - Ok(server_dir) -} - fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<()> { log::info!("killing existing server with PID {}", pid); std::process::Command::new("kill") @@ -453,7 +504,7 @@ fn spawn_server(paths: &ServerPaths) -> Result<()> { } let binary_name = std::env::current_exe()?; - let server_process = std::process::Command::new(binary_name) + let server_process = smol::process::Command::new(binary_name) .arg("run") .arg("--log-file") .arg(&paths.log_file) @@ -484,6 +535,7 @@ fn spawn_server(paths: &ServerPaths) -> Result<()> { "server ready to accept connections. total time waited: {:?}", total_time_waited ); + Ok(()) } @@ -556,3 +608,118 @@ async fn write_size_prefixed_buffer( stream.write_all(buffer).await?; Ok(()) } + +fn initialize_settings( + session: Arc, + fs: Arc, + cx: &mut AppContext, +) -> async_watch::Receiver> { + let user_settings_file_rx = watch_config_file( + &cx.background_executor(), + fs, + paths::settings_file().clone(), + ); + + handle_settings_file_changes(user_settings_file_rx, cx, { + let session = session.clone(); + move |err, _cx| { + if let Some(e) = err { + log::info!("Server settings failed to change: {}", e); + + session + .send(proto::Toast { + project_id: SSH_PROJECT_ID, + notification_id: "server-settings-failed".to_string(), + message: format!( + "Error in settings on remote host {:?}: {}", + paths::settings_file(), + e + ), + }) + .log_err(); + } else { + session + .send(proto::HideToast { + project_id: SSH_PROJECT_ID, + notification_id: "server-settings-failed".to_string(), + }) + .log_err(); + } + } + }); + + let (tx, rx) = async_watch::channel(None); + cx.observe_global::(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.unwrap_or_default(), + // 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(); + }) + .detach(); + + rx +} + +pub fn handle_settings_file_changes( + mut server_settings_file: mpsc::UnboundedReceiver, + cx: &mut AppContext, + settings_changed: impl Fn(Option, &mut AppContext) + 'static, +) { + let server_settings_content = cx + .background_executor() + .block(server_settings_file.next()) + .unwrap(); + SettingsStore::update_global(cx, |store, cx| { + store + .set_server_settings(&server_settings_content, cx) + .log_err(); + }); + cx.spawn(move |cx| async move { + while let Some(server_settings_content) = server_settings_file.next().await { + let result = cx.update_global(|store: &mut SettingsStore, cx| { + let result = store.set_server_settings(&server_settings_content, cx); + if let Err(err) = &result { + log::error!("Failed to load server settings: {err}"); + } + settings_changed(result.err(), cx); + cx.refresh(); + }); + if result.is_err() { + break; // App dropped + } + } + }) + .detach(); +} + +fn read_proxy_settings(cx: &mut ModelContext<'_, HeadlessProject>) -> Option { + let proxy_str = ProxySettings::get_global(cx).proxy.to_owned(); + let proxy_url = proxy_str + .as_ref() + .and_then(|input: &String| { + input + .parse::() + .inspect_err(|e| log::error!("Error parsing proxy settings: {}", e)) + .ok() + }) + .or_else(read_proxy_from_env); + proxy_url +} diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 1092a9bd4d64ffb86979b1d280f7323f3bef6658..40c371d9951ff11a538b53336a2a2af18e9d965a 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -88,7 +88,11 @@ pub fn initial_user_settings_content() -> Cow<'static, str> { asset_str::("settings/initial_user_settings.json") } -pub fn initial_local_settings_content() -> Cow<'static, str> { +pub fn initial_server_settings_content() -> Cow<'static, str> { + asset_str::("settings/initial_server_settings.json") +} + +pub fn initial_project_settings_content() -> Cow<'static, str> { asset_str::("settings/initial_local_settings.json") } diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index c8768f007db352cb78b0d90d29ba2925002a7fae..60494a6aee2e9d37088d6470ed5ed83292b853fa 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -110,6 +110,8 @@ pub struct SettingsSources<'a, T> { pub user: Option<&'a T>, /// The user settings for the current release channel. pub release_channel: Option<&'a T>, + /// The server's settings. + pub server: Option<&'a T>, /// The project settings, ordered from least specific to most specific. pub project: &'a [&'a T], } @@ -126,6 +128,7 @@ impl<'a, T: Serialize> SettingsSources<'a, T> { .into_iter() .chain(self.user) .chain(self.release_channel) + .chain(self.server) .chain(self.project.iter().copied()) } @@ -162,6 +165,7 @@ 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), HashMap>, @@ -219,6 +223,7 @@ impl SettingsStore { setting_values: Default::default(), raw_default_settings: serde_json::json!({}), raw_user_settings: serde_json::json!({}), + raw_server_settings: None, raw_extension_settings: serde_json::json!({}), raw_local_settings: Default::default(), tab_size_callback: Default::default(), @@ -269,6 +274,13 @@ impl SettingsStore { .log_err(); } + let server_value = self + .raw_server_settings + .as_ref() + .and_then(|server_setting| { + setting_value.deserialize_setting(server_setting).log_err() + }); + let extension_value = setting_value .deserialize_setting(&self.raw_extension_settings) .log_err(); @@ -277,9 +289,10 @@ impl SettingsStore { .load_setting( SettingsSources { default: &default_settings, - release_channel: release_channel_value.as_ref(), extensions: extension_value.as_ref(), user: user_value.as_ref(), + release_channel: release_channel_value.as_ref(), + server: server_value.as_ref(), project: &[], }, cx, @@ -522,6 +535,29 @@ impl SettingsStore { Ok(()) } + pub fn set_server_settings( + &mut self, + server_settings_content: &str, + cx: &mut AppContext, + ) -> Result<()> { + let settings: Option = if server_settings_content.is_empty() { + None + } else { + parse_json_with_comments(server_settings_content)? + }; + + anyhow::ensure!( + settings + .as_ref() + .map(|value| value.is_object()) + .unwrap_or(true), + "settings must be an object" + ); + self.raw_server_settings = settings; + self.recompute_values(None, cx)?; + Ok(()) + } + /// Add or remove a set of local settings via a JSON string. pub fn set_local_settings( &mut self, @@ -530,8 +566,8 @@ impl SettingsStore { kind: LocalSettingsKind, settings_content: Option<&str>, cx: &mut AppContext, - ) -> Result<()> { - anyhow::ensure!( + ) -> std::result::Result<(), InvalidSettingsError> { + debug_assert!( kind != LocalSettingsKind::Tasks, "Attempted to submit tasks into the settings store" ); @@ -541,7 +577,13 @@ impl SettingsStore { .entry((root_id, directory_path.clone())) .or_default(); let changed = if settings_content.is_some_and(|content| !content.is_empty()) { - let new_contents = parse_json_with_comments(settings_content.unwrap())?; + let new_contents = + parse_json_with_comments(settings_content.unwrap()).map_err(|e| { + InvalidSettingsError::LocalSettings { + path: directory_path.join(local_settings_file_relative_path()), + message: e.to_string(), + } + })?; if Some(&new_contents) == raw_local_settings.get(&kind) { false } else { @@ -711,12 +753,16 @@ impl SettingsStore { &mut self, changed_local_path: Option<(WorktreeId, &Path)>, cx: &mut AppContext, - ) -> Result<()> { + ) -> Result<(), InvalidSettingsError> { // Reload the global and local values for every setting. let mut project_settings_stack = Vec::::new(); let mut paths_stack = Vec::>::new(); for setting_value in self.setting_values.values_mut() { - let default_settings = setting_value.deserialize_setting(&self.raw_default_settings)?; + let default_settings = setting_value + .deserialize_setting(&self.raw_default_settings) + .map_err(|e| InvalidSettingsError::DefaultSettings { + message: e.to_string(), + })?; let extension_settings = setting_value .deserialize_setting(&self.raw_extension_settings) @@ -725,12 +771,17 @@ impl SettingsStore { let user_settings = match setting_value.deserialize_setting(&self.raw_user_settings) { Ok(settings) => Some(settings), Err(error) => { - return Err(anyhow!(InvalidSettingsError::UserSettings { - message: error.to_string() - })); + return Err(InvalidSettingsError::UserSettings { + message: error.to_string(), + }); } }; + let server_settings = self + .raw_server_settings + .as_ref() + .and_then(|setting| setting_value.deserialize_setting(setting).log_err()); + let mut release_channel_settings = None; if let Some(release_settings) = &self .raw_user_settings @@ -753,6 +804,7 @@ impl SettingsStore { extensions: extension_settings.as_ref(), user: user_settings.as_ref(), release_channel: release_channel_settings.as_ref(), + server: server_settings.as_ref(), project: &[], }, cx, @@ -804,6 +856,7 @@ impl SettingsStore { extensions: extension_settings.as_ref(), user: user_settings.as_ref(), release_channel: release_channel_settings.as_ref(), + server: server_settings.as_ref(), project: &project_settings_stack.iter().collect::>(), }, cx, @@ -818,10 +871,10 @@ impl SettingsStore { } } Err(error) => { - return Err(anyhow!(InvalidSettingsError::LocalSettings { + return Err(InvalidSettingsError::LocalSettings { path: directory_path.join(local_settings_file_relative_path()), - message: error.to_string() - })); + message: error.to_string(), + }); } } } @@ -835,13 +888,17 @@ impl SettingsStore { pub enum InvalidSettingsError { LocalSettings { path: PathBuf, message: String }, UserSettings { message: String }, + ServerSettings { message: String }, + DefaultSettings { message: String }, } impl std::fmt::Display for InvalidSettingsError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { InvalidSettingsError::LocalSettings { message, .. } - | InvalidSettingsError::UserSettings { message } => { + | InvalidSettingsError::UserSettings { message } + | InvalidSettingsError::ServerSettings { message } + | InvalidSettingsError::DefaultSettings { message } => { write!(f, "{}", message) } } @@ -893,6 +950,9 @@ impl AnySettingValue for SettingValue { release_channel: values .release_channel .map(|value| value.0.downcast_ref::().unwrap()), + server: values + .server + .map(|value| value.0.downcast_ref::().unwrap()), project: values .project .iter() diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index e4be957a1b0553889c1fdfa4b6fdac405d36548e..49f7ba3c83676626ae69817add0d3f3d905db636 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -636,7 +636,12 @@ impl settings::Settings for ThemeSettings { unnecessary_code_fade: defaults.unnecessary_code_fade.unwrap_or(0.0), }; - for value in sources.user.into_iter().chain(sources.release_channel) { + for value in sources + .user + .into_iter() + .chain(sources.release_channel) + .chain(sources.server) + { if let Some(value) = value.ui_density { this.ui_density = value; } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 024379e6acf28b06e0abfd0a9995242428cde927..86a52aca255e53e6b47956104b64ec1f6db401b5 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1080,9 +1080,14 @@ impl Settings for VimModeSetting { type FileContent = Option; fn load(sources: SettingsSources, _: &mut AppContext) -> Result { - Ok(Self(sources.user.copied().flatten().unwrap_or( - sources.default.ok_or_else(Self::missing_default)?, - ))) + Ok(Self( + sources + .user + .or(sources.server) + .copied() + .flatten() + .unwrap_or(sources.default.ok_or_else(Self::missing_default)?), + )) } } diff --git a/crates/welcome/src/base_keymap_setting.rs b/crates/welcome/src/base_keymap_setting.rs index 1b52bbc9f94fbd942fb294a4530f110d4d8c714c..d212dd41703c89ac0bfbb98612efae7db15fc08d 100644 --- a/crates/welcome/src/base_keymap_setting.rs +++ b/crates/welcome/src/base_keymap_setting.rs @@ -96,6 +96,9 @@ impl Settings for BaseKeymap { if let Some(Some(user_value)) = sources.user.copied() { return Ok(user_value); } + if let Some(Some(server_value)) = sources.server.copied() { + return Ok(server_value); + } sources.default.ok_or_else(Self::missing_default) } } diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index ffab276dd1aedb0fadc6eed88d3618c6e3101b5d..eee3d16a4ab3363dbd63e2ec0fbd13b8a2812e5d 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -16,30 +16,27 @@ pub fn init(cx: &mut AppContext) { } #[derive(Debug, PartialEq, Clone)] -pub struct NotificationId { - /// A [`TypeId`] used to uniquely identify this notification. - type_id: TypeId, - /// A supplementary ID used to distinguish between multiple - /// notifications that have the same [`type_id`](Self::type_id); - id: Option, +pub enum NotificationId { + Unique(TypeId), + Composite(TypeId, ElementId), + Named(SharedString), } impl NotificationId { /// Returns a unique [`NotificationId`] for the given type. pub fn unique() -> Self { - Self { - type_id: TypeId::of::(), - id: None, - } + Self::Unique(TypeId::of::()) } /// Returns a [`NotificationId`] for the given type that is also identified /// by the provided ID. - pub fn identified(id: impl Into) -> Self { - Self { - type_id: TypeId::of::(), - id: Some(id.into()), - } + pub fn composite(id: impl Into) -> Self { + Self::Composite(TypeId::of::(), id.into()) + } + + /// Builds a `NotificationId` out of the given string. + pub fn named(id: SharedString) -> Self { + Self::Named(id) } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3c3b26b4c137d2c39388d59880ab4385aad2ff8f..ec4079ba9f8af32594a68a3b283253bd119d2807 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -65,7 +65,7 @@ use release_channel::ReleaseChannel; use remote::{SshClientDelegate, SshConnectionOptions}; use serde::Deserialize; use session::AppSession; -use settings::{InvalidSettingsError, Settings}; +use settings::Settings; use shared_screen::SharedScreen; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, @@ -839,31 +839,17 @@ impl Workspace { } } - project::Event::LocalSettingsUpdated(result) => { - struct LocalSettingsUpdated; - let id = NotificationId::unique::(); - - match result { - Err(InvalidSettingsError::LocalSettings { message, path }) => { - let full_message = - format!("Failed to set local settings in {:?}:\n{}", path, message); - this.show_notification(id, cx, |cx| { - cx.new_view(|_| MessageNotification::new(full_message.clone())) - }) - } - Err(_) => {} - Ok(_) => this.dismiss_notification(&id, cx), - } - } - - project::Event::Notification(message) => { - struct ProjectNotification; + project::Event::Toast { + notification_id, + message, + } => this.show_notification( + NotificationId::named(notification_id.clone()), + cx, + |cx| cx.new_view(|_| MessageNotification::new(message.clone())), + ), - this.show_notification( - NotificationId::unique::(), - cx, - |cx| cx.new_view(|_| MessageNotification::new(message.clone())), - ) + project::Event::HideToast { notification_id } => { + this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx) } project::Event::LanguageServerPrompt(request) => { @@ -874,7 +860,7 @@ impl Workspace { let id = hasher.finish(); this.show_notification( - NotificationId::identified::(id as usize), + NotificationId::composite::(id as usize), cx, |cx| { cx.new_view(|_| { @@ -1808,6 +1794,7 @@ impl Workspace { .flat_map(|pane| { pane.read(cx).items().filter_map(|item| { if item.is_dirty(cx) { + item.tab_description(0, cx); Some((pane.downgrade(), item.boxed_clone())) } else { None diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index f5bc3a1847c6d5f75f422f34fc1e3f7e42accd25..c33cef4a4b2555c6fe8c1bae7eb337ac865fefeb 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -27,13 +27,14 @@ use anyhow::Context as _; use assets::Assets; use futures::{channel::mpsc, select_biased, StreamExt}; use outline_panel::OutlinePanel; +use project::Item; use project_panel::ProjectPanel; use quick_action_bar::QuickActionBar; use release_channel::{AppCommitSha, ReleaseChannel}; use rope::Rope; use search::project_search::ProjectSearchBar; use settings::{ - initial_local_settings_content, initial_tasks_content, KeymapFile, Settings, SettingsStore, + initial_project_settings_content, initial_tasks_content, KeymapFile, Settings, SettingsStore, DEFAULT_KEYMAP_PATH, }; use std::any::TypeId; @@ -53,7 +54,9 @@ use workspace::{ open_new, AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings, }; use workspace::{notifications::DetachAndPromptErr, Pane}; -use zed_actions::{OpenAccountSettings, OpenBrowser, OpenSettings, OpenZedUrl, Quit}; +use zed_actions::{ + OpenAccountSettings, OpenBrowser, OpenServerSettings, OpenSettings, OpenZedUrl, Quit, +}; actions!( zed, @@ -64,8 +67,8 @@ actions!( Minimize, OpenDefaultKeymap, OpenDefaultSettings, - OpenLocalSettings, - OpenLocalTasks, + OpenProjectSettings, + OpenProjectTasks, OpenTasks, ResetDatabase, ShowAll, @@ -218,6 +221,7 @@ pub fn initialize_workspace( let handle = cx.view().downgrade(); cx.on_window_should_close(move |cx| { + handle .update(cx, |workspace, cx| { // We'll handle closing asynchronously @@ -428,8 +432,8 @@ pub fn initialize_workspace( ); }, ) - .register_action(open_local_settings_file) - .register_action(open_local_tasks_file) + .register_action(open_project_settings_file) + .register_action(open_project_tasks_file) .register_action( move |workspace: &mut Workspace, _: &OpenDefaultKeymap, @@ -521,6 +525,25 @@ pub fn initialize_workspace( } } }); + if workspace.project().read(cx).is_via_ssh() { + workspace.register_action({ + move |workspace, _: &OpenServerSettings, cx| { + let open_server_settings = workspace.project().update(cx, |project, cx| { + project.open_server_settings(cx) + }); + + cx.spawn(|workspace, mut cx| async move { + let buffer = open_server_settings.await?; + + workspace.update(&mut cx, |workspace, cx| { + workspace.open_path(buffer.read(cx).project_path(cx).expect("Settings file must have a location"), None, true, cx) + })?.await?; + + anyhow::Ok(()) + }).detach_and_log_err(cx); + } + }); + } workspace.focus_handle(cx).focus(cx); }) @@ -813,22 +836,22 @@ pub fn load_default_keymap(cx: &mut AppContext) { } } -fn open_local_settings_file( +fn open_project_settings_file( workspace: &mut Workspace, - _: &OpenLocalSettings, + _: &OpenProjectSettings, cx: &mut ViewContext, ) { open_local_file( workspace, local_settings_file_relative_path(), - initial_local_settings_content(), + initial_project_settings_content(), cx, ) } -fn open_local_tasks_file( +fn open_project_tasks_file( workspace: &mut Workspace, - _: &OpenLocalTasks, + _: &OpenProjectTasks, cx: &mut ViewContext, ) { open_local_file( diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index 2995595589d9c3900c4a161e1844a2f701c4de94..52e0eab3e3560ac62c7b288a4d85df13a608fe94 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -19,7 +19,7 @@ pub fn app_menus() -> Vec { MenuItem::action("Open Key Bindings", zed_actions::OpenKeymap), MenuItem::action("Open Default Settings", super::OpenDefaultSettings), MenuItem::action("Open Default Key Bindings", super::OpenDefaultKeymap), - MenuItem::action("Open Local Settings", super::OpenLocalSettings), + MenuItem::action("Open Project Settings", super::OpenProjectSettings), MenuItem::action("Select Theme...", theme_selector::Toggle::default()), ], }), diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 4bd4796091b988650cdcf274fb115e5dc1fa732f..cedacb6d8495c846d4a3d0d39a2f270b9b8fdbef 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -27,6 +27,7 @@ actions!( [ OpenSettings, OpenAccountSettings, + OpenServerSettings, Quit, OpenKeymap, About, diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 121e63c233e508af1aa3957276450dab91dc7022..37d86e6481978f41b5d50accaf1e79161c83b444 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -15,7 +15,7 @@ TBD: Add settings documentation about how settings are merged as overlays. E.g. Your settings file can be opened with {#kb zed::OpenSettings}. By default it is located at `~/.config/zed/settings.json`, though if you have XDG_CONFIG_HOME in your environment on Linux it will be at `$XDG_CONFIG_HOME/zed/settings.json` instead. -This configuration is merged with any local configuration inside your projects. You can open the project settings by running {#action zed::OpenLocalSettings} from the command palette. This will create a `.zed` directory containing`.zed/settings.json`. +This configuration is merged with any local configuration inside your projects. You can open the project settings by running {#action zed::OpenProjectSettings} from the command palette. This will create a `.zed` directory containing`.zed/settings.json`. Although most projects will only need one settings file at the root, you can add more local settings files for subdirectories as needed. Not all settings can be set in local files, just those that impact the behavior of the editor and language tooling. For example you can set `tab_size`, `formatter` etc. but not `theme`, `vim_mode` and similar.