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 <conrad@zed.dev>
Co-authored-by: Richard <richard@zed.dev>
This commit is contained in:
Mikayla Maki 2024-10-15 23:32:44 -07:00 committed by GitHub
parent 1dda039f38
commit f944ebc4cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 804 additions and 218 deletions

4
Cargo.lock generated
View file

@ -8977,6 +8977,7 @@ dependencies = [
"log", "log",
"menu", "menu",
"ordered-float 2.10.1", "ordered-float 2.10.1",
"paths",
"picker", "picker",
"project", "project",
"release_channel", "release_channel",
@ -9136,6 +9137,7 @@ name = "remote_server"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-watch",
"backtrace", "backtrace",
"cargo_toml", "cargo_toml",
"clap", "clap",
@ -9151,8 +9153,10 @@ dependencies = [
"log", "log",
"lsp", "lsp",
"node_runtime", "node_runtime",
"paths",
"project", "project",
"remote", "remote",
"reqwest_client",
"rpc", "rpc",
"rust-embed", "rust-embed",
"serde", "serde",

View file

@ -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": {}
}

View file

@ -2278,7 +2278,7 @@ impl InlineAssist {
struct InlineAssistantError; struct InlineAssistantError;
let id = let id =
NotificationId::identified::<InlineAssistantError>( NotificationId::composite::<InlineAssistantError>(
assist_id.0, assist_id.0,
); );

View file

@ -38,7 +38,10 @@ impl Settings for SlashCommandSettings {
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut AppContext) -> Result<Self> { fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut AppContext) -> Result<Self> {
SettingsSources::<Self::FileContent>::json_merge_with( SettingsSources::<Self::FileContent>::json_merge_with(
[sources.default].into_iter().chain(sources.user), [sources.default]
.into_iter()
.chain(sources.user)
.chain(sources.server),
) )
} }
} }

View file

@ -414,7 +414,7 @@ impl TerminalInlineAssist {
struct InlineAssistantError; struct InlineAssistantError;
let id = let id =
NotificationId::identified::<InlineAssistantError>( NotificationId::composite::<InlineAssistantError>(
assist_id.0, assist_id.0,
); );

View file

@ -130,7 +130,7 @@ impl Settings for AutoUpdateSetting {
type FileContent = Option<AutoUpdateSettingContent>; type FileContent = Option<AutoUpdateSettingContent>;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> { fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
let auto_update = [sources.release_channel, sources.user] let auto_update = [sources.server, sources.release_channel, sources.user]
.into_iter() .into_iter()
.find_map(|value| value.copied().flatten()) .find_map(|value| value.copied().flatten())
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?); .unwrap_or(sources.default.ok_or_else(Self::missing_default)?);

View file

@ -141,6 +141,7 @@ impl Settings for ProxySettings {
Ok(Self { Ok(Self {
proxy: sources proxy: sources
.user .user
.or(sources.server)
.and_then(|value| value.proxy.clone()) .and_then(|value| value.proxy.clone())
.or(sources.default.proxy.clone()), .or(sources.default.proxy.clone()),
}) })
@ -472,15 +473,21 @@ impl settings::Settings for TelemetrySettings {
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> { fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
Ok(Self { Ok(Self {
diagnostics: sources.user.as_ref().and_then(|v| v.diagnostics).unwrap_or( diagnostics: sources
sources .user
.default .as_ref()
.diagnostics .or(sources.server.as_ref())
.ok_or_else(Self::missing_default)?, .and_then(|v| v.diagnostics)
), .unwrap_or(
sources
.default
.diagnostics
.ok_or_else(Self::missing_default)?,
),
metrics: sources metrics: sources
.user .user
.as_ref() .as_ref()
.or(sources.server.as_ref())
.and_then(|v| v.metrics) .and_then(|v| v.metrics)
.unwrap_or(sources.default.metrics.ok_or_else(Self::missing_default)?), .unwrap_or(sources.default.metrics.ok_or_else(Self::missing_default)?),
}) })

View file

@ -2,10 +2,12 @@ use crate::tests::TestServer;
use call::ActiveCall; use call::ActiveCall;
use fs::{FakeFs, Fs as _}; use fs::{FakeFs, Fs as _};
use gpui::{Context as _, TestAppContext}; 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 project::ProjectPath;
use remote::SshRemoteClient; use remote::SshRemoteClient;
use remote_server::HeadlessProject; use remote_server::{HeadlessAppState, HeadlessProject};
use serde_json::json; use serde_json::json;
use std::{path::Path, sync::Arc}; 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. // User A connects to the remote project via SSH.
server_cx.update(HeadlessProject::init); server_cx.update(HeadlessProject::init);
let _headless_project = let remote_http_client = Arc::new(BlockedHttpClient);
server_cx.new_model(|cx| HeadlessProject::new(server_ssh, remote_fs.clone(), cx)); 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 let (project_a, worktree_id) = client_a
.build_ssh_project("/code/project1", client_ssh, cx_a) .build_ssh_project("/code/project1", client_ssh, cx_a)

View file

@ -403,7 +403,10 @@ impl GitBlame {
if this.user_triggered { if this.user_triggered {
log::error!("failed to get git blame data: {error:?}"); log::error!("failed to get git blame data: {error:?}");
let notification = format!("{:#}", error).trim().to_string(); 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 { } else {
// If we weren't triggered by a user, we just log errors in the background, instead of sending // If we weren't triggered by a user, we just log errors in the background, instead of sending
// notifications. // notifications.
@ -619,9 +622,11 @@ mod tests {
let event = project.next_event(cx).await; let event = project.next_event(cx).await;
assert_eq!( assert_eq!(
event, event,
project::Event::Notification( project::Event::Toast {
"Failed to blame \"file.txt\": failed to get blame for \"file.txt\"".to_string() 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| { blame.update(cx, |blame, cx| {

View file

@ -42,7 +42,10 @@ impl Settings for ExtensionSettings {
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut AppContext) -> Result<Self> { fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut AppContext) -> Result<Self> {
SettingsSources::<Self::FileContent>::json_merge_with( SettingsSources::<Self::FileContent>::json_merge_with(
[sources.default].into_iter().chain(sources.user), [sources.default]
.into_iter()
.chain(sources.user)
.chain(sources.server),
) )
} }
} }

View file

@ -163,7 +163,7 @@ pub(crate) fn suggest(buffer: Model<Buffer>, cx: &mut ViewContext<Workspace>) {
struct ExtensionSuggestionNotification; struct ExtensionSuggestionNotification;
let notification_id = NotificationId::identified::<ExtensionSuggestionNotification>( let notification_id = NotificationId::composite::<ExtensionSuggestionNotification>(
SharedString::from(extension_id.clone()), SharedString::from(extension_id.clone()),
); );

View file

@ -567,6 +567,12 @@ pub struct WeakModel<T> {
entity_type: PhantomData<T>, entity_type: PhantomData<T>,
} }
impl<T> std::fmt::Debug for WeakModel<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct(type_name::<WeakModel<T>>()).finish()
}
}
unsafe impl<T> Send for WeakModel<T> {} unsafe impl<T> Send for WeakModel<T> {}
unsafe impl<T> Sync for WeakModel<T> {} unsafe impl<T> Sync for WeakModel<T> {}

View file

@ -317,6 +317,12 @@ pub fn read_proxy_from_env() -> Option<Uri> {
pub struct BlockedHttpClient; pub struct BlockedHttpClient;
impl BlockedHttpClient {
pub fn new() -> Self {
BlockedHttpClient
}
}
impl HttpClient for BlockedHttpClient { impl HttpClient for BlockedHttpClient {
fn send( fn send(
&self, &self,

View file

@ -367,7 +367,7 @@ pub trait LspAdapter: 'static + Send + Sync {
} }
let Some(container_dir) = delegate.language_server_download_dir(&self.name()).await else { 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; let mut binary = try_fetch_server_binary(self.as_ref(), &delegate, container_dir.to_path_buf(), cx).await;

View file

@ -5,6 +5,11 @@ use std::sync::OnceLock;
pub use util::paths::home_dir; 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. /// Returns the path to the configuration directory used by Zed.
pub fn config_dir() -> &'static PathBuf { pub fn config_dir() -> &'static PathBuf {
static CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new(); static CONFIG_DIR: OnceLock<PathBuf> = 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<PathBuf> = OnceLock::new();
REMOTE_SERVER_STATE.get_or_init(|| return support_dir().join("server_state"))
}
/// Returns the path to the `Zed.log` file. /// Returns the path to the `Zed.log` file.
pub fn log_file() -> &'static PathBuf { pub fn log_file() -> &'static PathBuf {
static LOG_FILE: OnceLock<PathBuf> = OnceLock::new(); static LOG_FILE: OnceLock<PathBuf> = OnceLock::new();

View file

@ -27,7 +27,7 @@ use gpui::{
AppContext, AsyncAppContext, Context, Entity, EventEmitter, Model, ModelContext, PromptLevel, AppContext, AsyncAppContext, Context, Entity, EventEmitter, Model, ModelContext, PromptLevel,
Task, WeakModel, Task, WeakModel,
}; };
use http_client::{BlockedHttpClient, HttpClient}; use http_client::HttpClient;
use language::{ use language::{
language_settings::{ language_settings::{
all_language_settings, language_settings, AllLanguageSettings, FormatOnSave, Formatter, all_language_settings, language_settings, AllLanguageSettings, FormatOnSave, Formatter,
@ -116,7 +116,7 @@ impl FormatTrigger {
} }
pub struct LocalLspStore { pub struct LocalLspStore {
http_client: Option<Arc<dyn HttpClient>>, http_client: Arc<dyn HttpClient>,
environment: Model<ProjectEnvironment>, environment: Model<ProjectEnvironment>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
yarn: Model<YarnPathStore>, yarn: Model<YarnPathStore>,
@ -839,7 +839,7 @@ impl LspStore {
prettier_store: Model<PrettierStore>, prettier_store: Model<PrettierStore>,
environment: Model<ProjectEnvironment>, environment: Model<ProjectEnvironment>,
languages: Arc<LanguageRegistry>, languages: Arc<LanguageRegistry>,
http_client: Option<Arc<dyn HttpClient>>, http_client: Arc<dyn HttpClient>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Self { ) -> Self {
@ -7579,10 +7579,7 @@ impl LocalLspAdapterDelegate {
.as_local() .as_local()
.expect("LocalLspAdapterDelegate cannot be constructed on a remote"); .expect("LocalLspAdapterDelegate cannot be constructed on a remote");
let http_client = local let http_client = local.http_client.clone();
.http_client
.clone()
.unwrap_or_else(|| Arc::new(BlockedHttpClient));
Self::new(lsp_store, worktree, http_client, local.fs.clone(), cx) Self::new(lsp_store, worktree, http_client, local.fs.clone(), cx)
} }

View file

@ -222,8 +222,13 @@ pub enum Event {
LanguageServerAdded(LanguageServerId, LanguageServerName, Option<WorktreeId>), LanguageServerAdded(LanguageServerId, LanguageServerName, Option<WorktreeId>),
LanguageServerRemoved(LanguageServerId), LanguageServerRemoved(LanguageServerId),
LanguageServerLog(LanguageServerId, LanguageServerLogType, String), LanguageServerLog(LanguageServerId, LanguageServerLogType, String),
Notification(String), Toast {
LocalSettingsUpdated(Result<(), InvalidSettingsError>), notification_id: SharedString,
message: String,
},
HideToast {
notification_id: SharedString,
},
LanguageServerPrompt(LanguageServerPromptRequest), LanguageServerPrompt(LanguageServerPromptRequest),
LanguageNotFound(Model<Buffer>), LanguageNotFound(Model<Buffer>),
ActiveEntryChanged(Option<ProjectEntryId>), ActiveEntryChanged(Option<ProjectEntryId>),
@ -633,7 +638,7 @@ impl Project {
prettier_store.clone(), prettier_store.clone(),
environment.clone(), environment.clone(),
languages.clone(), languages.clone(),
Some(client.http_client()), client.http_client(),
fs.clone(), fs.clone(),
cx, cx,
) )
@ -694,7 +699,7 @@ impl Project {
let snippets = let snippets =
SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx); 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 = let worktree_store =
cx.new_model(|_| WorktreeStore::remote(false, ssh_proto.clone(), 0, None)); cx.new_model(|_| WorktreeStore::remote(false, ssh_proto.clone(), 0, None));
cx.subscribe(&worktree_store, Self::on_worktree_store_event) cx.subscribe(&worktree_store, Self::on_worktree_store_event)
@ -703,7 +708,7 @@ impl Project {
let buffer_store = cx.new_model(|cx| { let buffer_store = cx.new_model(|cx| {
BufferStore::remote( BufferStore::remote(
worktree_store.clone(), worktree_store.clone(),
ssh.read(cx).to_proto_client(), ssh.read(cx).proto_client(),
SSH_PROJECT_ID, SSH_PROJECT_ID,
cx, cx,
) )
@ -716,7 +721,7 @@ impl Project {
fs.clone(), fs.clone(),
buffer_store.downgrade(), buffer_store.downgrade(),
worktree_store.clone(), worktree_store.clone(),
ssh.read(cx).to_proto_client(), ssh.read(cx).proto_client(),
SSH_PROJECT_ID, SSH_PROJECT_ID,
cx, 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_create_buffer_for_peer);
ssh_proto.add_model_message_handler(Self::handle_update_worktree); 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_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); ssh_proto.add_model_request_handler(BufferStore::handle_update_buffer);
BufferStore::init(&ssh_proto); BufferStore::init(&ssh_proto);
LspStore::init(&ssh_proto); LspStore::init(&ssh_proto);
@ -2065,7 +2072,7 @@ impl Project {
if let Some(ref ssh_client) = self.ssh_client { if let Some(ref ssh_client) = self.ssh_client {
ssh_client ssh_client
.read(cx) .read(cx)
.to_proto_client() .proto_client()
.send(proto::CloseBuffer { .send(proto::CloseBuffer {
project_id: 0, project_id: 0,
buffer_id: buffer_id.to_proto(), buffer_id: buffer_id.to_proto(),
@ -2136,7 +2143,10 @@ impl Project {
.ok(); .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 { LspStoreEvent::SnippetEdit {
buffer_id, buffer_id,
edits, edits,
@ -2180,9 +2190,20 @@ impl Project {
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
match event { match event {
SettingsObserverEvent::LocalSettingsUpdated(error) => { SettingsObserverEvent::LocalSettingsUpdated(result) => match result {
cx.emit(Event::LocalSettingsUpdated(error.clone())) 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 { if let Some(ssh) = &self.ssh_client {
ssh.read(cx) ssh.read(cx)
.to_proto_client() .proto_client()
.send(proto::RemoveWorktree { .send(proto::RemoveWorktree {
worktree_id: id_to_remove.to_proto(), worktree_id: id_to_remove.to_proto(),
}) })
@ -2295,7 +2316,7 @@ impl Project {
if let Some(ssh) = &self.ssh_client { if let Some(ssh) = &self.ssh_client {
ssh.read(cx) ssh.read(cx)
.to_proto_client() .proto_client()
.send(proto::UpdateBuffer { .send(proto::UpdateBuffer {
project_id: 0, project_id: 0,
buffer_id: buffer_id.to_proto(), buffer_id: buffer_id.to_proto(),
@ -2632,6 +2653,35 @@ impl Project {
}) })
} }
pub fn open_server_settings(
&mut self,
cx: &mut ModelContext<Self>,
) -> Task<Result<Model<Buffer>>> {
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( pub fn open_local_buffer_via_lsp(
&mut self, &mut self,
abs_path: lsp::Url, abs_path: lsp::Url,
@ -2982,7 +3032,7 @@ impl Project {
let (tx, rx) = smol::channel::unbounded(); let (tx, rx) = smol::channel::unbounded();
let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.ssh_client { 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() { } else if let Some(remote_id) = self.remote_id() {
(self.client.clone().into(), remote_id) (self.client.clone().into(), remote_id)
} else { } else {
@ -3069,14 +3119,9 @@ impl Project {
visible: bool, visible: bool,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<(Model<Worktree>, PathBuf)>> { ) -> Task<Result<(Model<Worktree>, PathBuf)>> {
let abs_path = abs_path.as_ref(); self.worktree_store.update(cx, |worktree_store, cx| {
if let Some((tree, relative_path)) = self.find_worktree(abs_path, cx) { worktree_store.find_or_create_worktree(abs_path, visible, 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 find_worktree( pub fn find_worktree(
@ -3138,7 +3183,7 @@ impl Project {
} else if let Some(ssh_client) = self.ssh_client.as_ref() { } else if let Some(ssh_client) = self.ssh_client.as_ref() {
let request = ssh_client let request = ssh_client
.read(cx) .read(cx)
.to_proto_client() .proto_client()
.request(proto::CheckFileExists { .request(proto::CheckFileExists {
project_id: SSH_PROJECT_ID, project_id: SSH_PROJECT_ID,
path: path.to_string(), path: path.to_string(),
@ -3215,7 +3260,7 @@ impl Project {
path: query, 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 { cx.background_executor().spawn(async move {
let response = response.await?; let response = response.await?;
Ok(response.entries.into_iter().map(PathBuf::from).collect()) Ok(response.entries.into_iter().map(PathBuf::from).collect())
@ -3239,7 +3284,7 @@ impl Project {
} }
} }
fn create_worktree( pub fn create_worktree(
&mut self, &mut self,
abs_path: impl AsRef<Path>, abs_path: impl AsRef<Path>,
visible: bool, visible: bool,
@ -3544,6 +3589,33 @@ impl Project {
})? })?
} }
async fn handle_toast(
this: Model<Self>,
envelope: TypedEnvelope<proto::Toast>,
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<Self>,
envelope: TypedEnvelope<proto::HideToast>,
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 // Collab sends UpdateWorktree protos as messages
async fn handle_update_worktree( async fn handle_update_worktree(
this: Model<Self>, this: Model<Self>,
@ -3572,7 +3644,7 @@ impl Project {
let mut payload = envelope.payload.clone(); let mut payload = envelope.payload.clone();
payload.project_id = SSH_PROJECT_ID; payload.project_id = SSH_PROJECT_ID;
cx.background_executor() 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); .detach_and_log_err(cx);
} }
this.buffer_store.clone() this.buffer_store.clone()

View file

@ -538,26 +538,47 @@ impl SettingsObserver {
let task_store = self.task_store.clone(); let task_store = self.task_store.clone();
for (directory, kind, file_content) in settings_contents { for (directory, kind, file_content) in settings_contents {
let result = match kind { match kind {
LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
.update_global::<SettingsStore, anyhow::Result<()>>(|store, cx| { .update_global::<SettingsStore, _>(|store, cx| {
store.set_local_settings( let result = store.set_local_settings(
worktree_id, worktree_id,
directory.clone(), directory.clone(),
kind, kind,
file_content.as_deref(), file_content.as_deref(),
cx, 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| { LocalSettingsKind::Tasks => task_store.update(cx, |task_store, cx| {
task_store.update_user_tasks( task_store
Some(SettingsLocation { .update_user_tasks(
worktree_id, Some(SettingsLocation {
path: directory.as_ref(), worktree_id,
}), path: directory.as_ref(),
file_content.as_deref(), }),
cx, file_content.as_deref(),
) cx,
)
.log_err();
}), }),
}; };
@ -572,28 +593,6 @@ impl SettingsObserver {
}) })
.log_err(); .log_err();
} }
match result {
Err(error) => {
if let Ok(error) = error.downcast::<InvalidSettingsError>() {
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(())));
}
}
} }
} }
} }

View file

@ -298,9 +298,10 @@ impl TaskStore {
let result = task_store.update_user_tasks(None, Some(&user_tasks_content), cx); let result = task_store.update_user_tasks(None, Some(&user_tasks_content), cx);
if let Err(err) = &result { if let Err(err) = &result {
log::error!("Failed to load user tasks: {err}"); log::error!("Failed to load user tasks: {err}");
cx.emit(crate::Event::Notification(format!( cx.emit(crate::Event::Toast {
"Invalid global tasks file\n{err}" notification_id: "load-user-tasks".into(),
))); message: format!("Invalid global tasks file\n{err}"),
});
} }
cx.refresh(); cx.refresh();
}) else { }) else {

View file

@ -367,7 +367,11 @@ pub fn wrap_for_ssh(
// replace ith with something that works // replace ith with something that works
let tilde_prefix = "~/"; let tilde_prefix = "~/";
if path.starts_with(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}") format!("cd \"$HOME/{trimmed_path}\"; {env_changes} {to_run}")
} else { } else {
format!("cd {path:?}; {env_changes} {to_run}") format!("cd {path:?}; {env_changes} {to_run}")

View file

@ -153,6 +153,22 @@ impl WorktreeStore {
None None
} }
pub fn find_or_create_worktree(
&mut self,
abs_path: impl AsRef<Path>,
visible: bool,
cx: &mut ModelContext<Self>,
) -> Task<Result<(Model<Worktree>, 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>( pub fn entry_for_id<'a>(
&'a self, &'a self,
entry_id: ProjectEntryId, entry_id: ProjectEntryId,
@ -957,7 +973,7 @@ impl WorktreeStore {
} }
} }
#[derive(Clone)] #[derive(Clone, Debug)]
enum WorktreeHandle { enum WorktreeHandle {
Strong(Model<Worktree>), Strong(Model<Worktree>),
Weak(WeakModel<Worktree>), Weak(WeakModel<Worktree>),

View file

@ -880,9 +880,10 @@ impl ProjectPanel {
if is_dir { if is_dir {
project_panel.project.update(cx, |_, cx| { project_panel.project.update(cx, |_, cx| {
cx.emit(project::Event::Notification(format!( cx.emit(project::Event::Toast {
"Created an excluded directory at {abs_path:?}.\nAlter `file_scan_exclusions` in the settings to show it in the panel" 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 None
} else { } else {

View file

@ -287,7 +287,12 @@ message Envelope {
RemoveWorktree remove_worktree = 258; 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; reserved 87 to 88;
@ -2487,3 +2492,18 @@ message ShutdownRemoteServer {}
message RemoveWorktree { message RemoveWorktree {
uint64 worktree_id = 1; 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;
}

View file

@ -367,6 +367,9 @@ messages!(
(ShutdownRemoteServer, Foreground), (ShutdownRemoteServer, Foreground),
(RemoveWorktree, Foreground), (RemoveWorktree, Foreground),
(LanguageServerLog, Foreground), (LanguageServerLog, Foreground),
(Toast, Background),
(HideToast, Background),
(OpenServerSettings, Foreground),
); );
request_messages!( request_messages!(
@ -490,7 +493,8 @@ request_messages!(
(AddWorktree, AddWorktreeResponse), (AddWorktree, AddWorktreeResponse),
(CheckFileExists, CheckFileExistsResponse), (CheckFileExists, CheckFileExistsResponse),
(ShutdownRemoteServer, Ack), (ShutdownRemoteServer, Ack),
(RemoveWorktree, Ack) (RemoveWorktree, Ack),
(OpenServerSettings, OpenBufferResponse)
); );
entity_messages!( entity_messages!(
@ -564,6 +568,10 @@ entity_messages!(
UpdateUserSettings, UpdateUserSettings,
CheckFileExists, CheckFileExists,
LanguageServerLog, LanguageServerLog,
Toast,
HideToast,
OpenServerSettings,
); );
entity_messages!( entity_messages!(

View file

@ -39,6 +39,7 @@ terminal_view.workspace = true
ui.workspace = true ui.workspace = true
util.workspace = true util.workspace = true
workspace.workspace = true workspace.workspace = true
paths.workspace = true
[dev-dependencies] [dev-dependencies]
editor = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] }

View file

@ -948,7 +948,7 @@ impl DevServerProjects {
this.show_toast( this.show_toast(
Toast::new( Toast::new(
NotificationId::identified::< NotificationId::composite::<
SshServerAddressCopiedToClipboard, SshServerAddressCopiedToClipboard,
>( >(
connection_string.clone() connection_string.clone()
@ -1002,7 +1002,7 @@ impl DevServerProjects {
); );
this.show_toast( this.show_toast(
Toast::new( Toast::new(
NotificationId::identified::<SshServerRemoval>( NotificationId::composite::<SshServerRemoval>(
connection_string.clone(), connection_string.clone(),
), ),
notification, notification,

View file

@ -10,6 +10,7 @@ use gpui::{
Transformation, View, Transformation, View,
}; };
use gpui::{AppContext, Model}; use gpui::{AppContext, Model};
use release_channel::{AppVersion, ReleaseChannel}; use release_channel::{AppVersion, ReleaseChannel};
use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient}; use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient};
use schemars::JsonSchema; use schemars::JsonSchema;
@ -377,9 +378,18 @@ impl remote::SshClientDelegate for SshClientDelegate {
rx rx
} }
fn remote_server_binary_path(&self, cx: &mut AsyncAppContext) -> Result<PathBuf> { fn remote_server_binary_path(
&self,
platform: SshPlatform,
cx: &mut AsyncAppContext,
) -> Result<PathBuf> {
let release_channel = cx.update(|cx| ReleaseChannel::global(cx))?; 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"); let path = std::env::current_dir()?.join("target/remote_server/debug/remote_server.gz");
return Ok(Some((path, version))); return Ok(Some((path, version)));
} else if let Some(triple) = platform.triple() { } 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); self.update_status(Some("Installing cross.rs for cross-compilation"), cx);
log::info!("installing cross"); log::info!("installing cross");

View file

@ -137,7 +137,11 @@ pub trait SshClientDelegate: Send + Sync {
prompt: String, prompt: String,
cx: &mut AsyncAppContext, cx: &mut AsyncAppContext,
) -> oneshot::Receiver<Result<String>>; ) -> oneshot::Receiver<Result<String>>;
fn remote_server_binary_path(&self, cx: &mut AsyncAppContext) -> Result<PathBuf>; fn remote_server_binary_path(
&self,
platform: SshPlatform,
cx: &mut AsyncAppContext,
) -> Result<PathBuf>;
fn get_server_binary( fn get_server_binary(
&self, &self,
platform: SshPlatform, platform: SshPlatform,
@ -972,7 +976,7 @@ impl SshRemoteClient {
let platform = ssh_connection.query_platform().await?; let platform = ssh_connection.query_platform().await?;
let (local_binary_path, version) = delegate.get_server_binary(platform, cx).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 ssh_connection
.ensure_server_binary( .ensure_server_binary(
&delegate, &delegate,
@ -1021,7 +1025,7 @@ impl SshRemoteClient {
.map(|ssh_connection| ssh_connection.socket.ssh_args()) .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() self.client.clone().into()
} }

View file

@ -22,6 +22,7 @@ debug-embed = ["dep:rust-embed"]
test-support = ["fs/test-support"] test-support = ["fs/test-support"]
[dependencies] [dependencies]
async-watch.workspace = true
anyhow.workspace = true anyhow.workspace = true
backtrace = "0.3" backtrace = "0.3"
clap.workspace = true clap.workspace = true
@ -30,13 +31,16 @@ env_logger.workspace = true
fs.workspace = true fs.workspace = true
futures.workspace = true futures.workspace = true
gpui.workspace = true gpui.workspace = true
http_client.workspace = true
language.workspace = true language.workspace = true
languages.workspace = true languages.workspace = true
log.workspace = true log.workspace = true
lsp.workspace = true lsp.workspace = true
node_runtime.workspace = true node_runtime.workspace = true
project.workspace = true project.workspace = true
paths = { workspace = true }
remote.workspace = true remote.workspace = true
reqwest_client.workspace = true
rpc.workspace = true rpc.workspace = true
rust-embed = { workspace = true, optional = true, features = ["debug-embed"] } rust-embed = { workspace = true, optional = true, features = ["debug-embed"] }
serde.workspace = true serde.workspace = true
@ -66,4 +70,4 @@ cargo_toml.workspace = true
toml.workspace = true toml.workspace = true
[package.metadata.cargo-machete] [package.metadata.cargo-machete]
ignored = ["rust-embed"] ignored = ["rust-embed", "paths"]

View file

@ -1,6 +1,7 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use fs::Fs; use fs::Fs;
use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext}; use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext};
use http_client::HttpClient;
use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry}; use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry};
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
use project::{ use project::{
@ -16,6 +17,8 @@ use rpc::{
proto::{self, SSH_PEER_ID, SSH_PROJECT_ID}, proto::{self, SSH_PEER_ID, SSH_PROJECT_ID},
AnyProtoClient, TypedEnvelope, AnyProtoClient, TypedEnvelope,
}; };
use settings::initial_server_settings_content;
use smol::stream::StreamExt; use smol::stream::StreamExt;
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -36,6 +39,14 @@ pub struct HeadlessProject {
pub languages: Arc<LanguageRegistry>, pub languages: Arc<LanguageRegistry>,
} }
pub struct HeadlessAppState {
pub session: Arc<ChannelClient>,
pub fs: Arc<dyn Fs>,
pub http_client: Arc<dyn HttpClient>,
pub node_runtime: NodeRuntime,
pub languages: Arc<LanguageRegistry>,
}
impl HeadlessProject { impl HeadlessProject {
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
settings::init(cx); settings::init(cx);
@ -43,11 +54,16 @@ impl HeadlessProject {
project::Project::init_settings(cx); project::Project::init_settings(cx);
} }
pub fn new(session: Arc<ChannelClient>, fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self { pub fn new(
let languages = Arc::new(LanguageRegistry::new(cx.background_executor().clone())); HeadlessAppState {
session,
let node_runtime = NodeRuntime::unavailable(); fs,
http_client,
node_runtime,
languages,
}: HeadlessAppState,
cx: &mut ModelContext<Self>,
) -> Self {
languages::init(languages.clone(), node_runtime.clone(), cx); languages::init(languages.clone(), node_runtime.clone(), cx);
let worktree_store = cx.new_model(|cx| { let worktree_store = cx.new_model(|cx| {
@ -99,7 +115,7 @@ impl HeadlessProject {
prettier_store.clone(), prettier_store.clone(),
environment, environment,
languages.clone(), languages.clone(),
None, http_client,
fs.clone(), fs.clone(),
cx, 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_open_buffer_by_path);
client.add_model_request_handler(Self::handle_find_search_candidates); 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_request_handler(BufferStore::handle_update_buffer);
client.add_model_message_handler(BufferStore::handle_close_buffer); client.add_model_message_handler(BufferStore::handle_close_buffer);
@ -203,6 +220,15 @@ impl HeadlessProject {
}) })
.log_err(); .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) => { LspStoreEvent::LanguageServerLog(language_server_id, log_type, message) => {
self.session self.session
.send(proto::LanguageServerLog { .send(proto::LanguageServerLog {
@ -336,6 +362,59 @@ impl HeadlessProject {
}) })
} }
pub async fn handle_open_server_settings(
this: Model<Self>,
_: TypedEnvelope<proto::OpenServerSettings>,
mut cx: AsyncAppContext,
) -> Result<proto::OpenBufferResponse> {
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( pub async fn handle_find_search_candidates(
this: Model<Self>, this: Model<Self>,
envelope: TypedEnvelope<proto::FindSearchCandidates>, envelope: TypedEnvelope<proto::FindSearchCandidates>,

View file

@ -3,7 +3,7 @@ use client::{Client, UserStore};
use clock::FakeSystemClock; use clock::FakeSystemClock;
use fs::{FakeFs, Fs}; use fs::{FakeFs, Fs};
use gpui::{Context, Model, TestAppContext}; use gpui::{Context, Model, TestAppContext};
use http_client::FakeHttpClient; use http_client::{BlockedHttpClient, FakeHttpClient};
use language::{ use language::{
language_settings::{all_language_settings, AllLanguageSettings}, language_settings::{all_language_settings, AllLanguageSettings},
Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LanguageServerName, Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LanguageServerName,
@ -17,7 +17,7 @@ use project::{
}; };
use remote::SshRemoteClient; use remote::SshRemoteClient;
use serde_json::json; use serde_json::json;
use settings::{Settings, SettingsLocation, SettingsStore}; use settings::{initial_server_settings_content, Settings, SettingsLocation, SettingsStore};
use smol::stream::StreamExt; use smol::stream::StreamExt;
use std::{ use std::{
path::{Path, PathBuf}, 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| { cx.update_global(|settings_store: &mut SettingsStore, cx| {
settings_store.set_user_settings( settings_store.set_user_settings(
r#"{"languages":{"Rust":{"language_servers":["custom-rust-analyzer"]}}}"#, r#"{"languages":{"Rust":{"language_servers":["from-local-settings"]}}}"#,
cx, cx,
) )
}) })
@ -210,7 +210,27 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
AllLanguageSettings::get_global(cx) AllLanguageSettings::get_global(cx)
.language(Some(&"Rust".into())) .language(Some(&"Rust".into()))
.language_servers, .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() { fn init_logger() {
if std::env::var("RUST_LOG").is_ok() { if std::env::var("RUST_LOG").is_ok() {
env_logger::try_init().ok(); env_logger::try_init().ok();
@ -642,8 +677,23 @@ async fn init_test(
); );
server_cx.update(HeadlessProject::init); server_cx.update(HeadlessProject::init);
let headless = let http_client = Arc::new(BlockedHttpClient);
server_cx.new_model(|cx| HeadlessProject::new(ssh_server_client, fs.clone(), cx)); 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); let project = build_project(ssh_remote_client, cx);
project project

View file

@ -6,4 +6,4 @@ pub mod unix;
#[cfg(test)] #[cfg(test)]
mod remote_editing_tests; mod remote_editing_tests;
pub use headless_project::HeadlessProject; pub use headless_project::{HeadlessAppState, HeadlessProject};

View file

@ -1,27 +1,37 @@
use crate::headless_project::HeadlessAppState;
use crate::HeadlessProject; use crate::HeadlessProject;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use fs::RealFs; use client::ProxySettings;
use fs::{Fs, RealFs};
use futures::channel::mpsc; use futures::channel::mpsc;
use futures::{select, select_biased, AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt, SinkExt}; 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::proxy::ProxyLaunchError;
use remote::ssh_session::ChannelClient; use remote::ssh_session::ChannelClient;
use remote::{ use remote::{
json_log::LogRecord, json_log::LogRecord,
protocol::{read_message, write_message}, 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::channel::{Receiver, Sender};
use smol::io::AsyncReadExt; use smol::io::AsyncReadExt;
use smol::Async; use smol::Async;
use smol::{net::unix::UnixListener, stream::StreamExt as _}; use smol::{net::unix::UnixListener, stream::StreamExt as _};
use std::{ use std::{
env,
io::Write, io::Write,
mem, mem,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
}; };
use util::ResultExt;
fn init_logging_proxy() { fn init_logging_proxy() {
env_logger::builder() env_logger::builder()
@ -266,6 +276,22 @@ fn start_server(
ChannelClient::new(incoming_rx, outgoing_tx, cx) 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( pub fn execute_run(
log_file: PathBuf, log_file: PathBuf,
pid_file: PathBuf, pid_file: PathBuf,
@ -275,6 +301,7 @@ pub fn execute_run(
) -> Result<()> { ) -> Result<()> {
let log_rx = init_logging_server(log_file)?; let log_rx = init_logging_server(log_file)?;
init_panic_hook(); init_panic_hook();
init_paths()?;
log::info!( log::info!(
"starting up. pid_file: {:?}, stdin_socket: {:?}, stdout_socket: {:?}, stderr_socket: {:?}", "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"); log::info!("gpui app started, initializing server");
let session = start_server(listeners, log_rx, cx); let session = start_server(listeners, log_rx, cx);
client::init_settings(cx);
let project = cx.new_model(|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); mem::forget(project);
@ -318,13 +380,15 @@ struct ServerPaths {
impl ServerPaths { impl ServerPaths {
fn new(identifier: &str) -> Result<Self> { fn new(identifier: &str) -> Result<Self> {
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 pid_file = server_dir.join("server.pid");
let stdin_socket = project_dir.join("stdin.sock"); let stdin_socket = server_dir.join("stdin.sock");
let stdout_socket = project_dir.join("stdout.sock"); let stdout_socket = server_dir.join("stdout.sock");
let stderr_socket = project_dir.join("stderr.sock"); let stderr_socket = server_dir.join("stderr.sock");
let log_file = project_dir.join("server.log"); let log_file = logs_dir().join(format!("server-{}.log", identifier));
Ok(Self { Ok(Self {
pid_file, pid_file,
@ -358,7 +422,7 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
} }
spawn_server(&server_paths)?; spawn_server(&server_paths)?;
} };
let stdin_task = smol::spawn(async move { let stdin_task = smol::spawn(async move {
let stdin = Async::new(std::io::stdin())?; let stdin = Async::new(std::io::stdin())?;
@ -409,19 +473,6 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
Ok(()) Ok(())
} }
fn create_state_directory(identifier: &str) -> Result<PathBuf> {
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<()> { fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<()> {
log::info!("killing existing server with PID {}", pid); log::info!("killing existing server with PID {}", pid);
std::process::Command::new("kill") std::process::Command::new("kill")
@ -453,7 +504,7 @@ fn spawn_server(paths: &ServerPaths) -> Result<()> {
} }
let binary_name = std::env::current_exe()?; 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("run")
.arg("--log-file") .arg("--log-file")
.arg(&paths.log_file) .arg(&paths.log_file)
@ -484,6 +535,7 @@ fn spawn_server(paths: &ServerPaths) -> Result<()> {
"server ready to accept connections. total time waited: {:?}", "server ready to accept connections. total time waited: {:?}",
total_time_waited total_time_waited
); );
Ok(()) Ok(())
} }
@ -556,3 +608,118 @@ async fn write_size_prefixed_buffer<S: AsyncWrite + Unpin>(
stream.write_all(buffer).await?; stream.write_all(buffer).await?;
Ok(()) Ok(())
} }
fn initialize_settings(
session: Arc<ChannelClient>,
fs: Arc<dyn Fs>,
cx: &mut AppContext,
) -> async_watch::Receiver<Option<NodeBinaryOptions>> {
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::<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.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<String>,
cx: &mut AppContext,
settings_changed: impl Fn(Option<anyhow::Error>, &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<Uri> {
let proxy_str = ProxySettings::get_global(cx).proxy.to_owned();
let proxy_url = proxy_str
.as_ref()
.and_then(|input: &String| {
input
.parse::<Uri>()
.inspect_err(|e| log::error!("Error parsing proxy settings: {}", e))
.ok()
})
.or_else(read_proxy_from_env);
proxy_url
}

View file

@ -88,7 +88,11 @@ pub fn initial_user_settings_content() -> Cow<'static, str> {
asset_str::<SettingsAssets>("settings/initial_user_settings.json") asset_str::<SettingsAssets>("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::<SettingsAssets>("settings/initial_server_settings.json")
}
pub fn initial_project_settings_content() -> Cow<'static, str> {
asset_str::<SettingsAssets>("settings/initial_local_settings.json") asset_str::<SettingsAssets>("settings/initial_local_settings.json")
} }

View file

@ -110,6 +110,8 @@ pub struct SettingsSources<'a, T> {
pub user: Option<&'a T>, pub user: Option<&'a T>,
/// The user settings for the current release channel. /// The user settings for the current release channel.
pub release_channel: Option<&'a T>, 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. /// The project settings, ordered from least specific to most specific.
pub project: &'a [&'a T], pub project: &'a [&'a T],
} }
@ -126,6 +128,7 @@ impl<'a, T: Serialize> SettingsSources<'a, T> {
.into_iter() .into_iter()
.chain(self.user) .chain(self.user)
.chain(self.release_channel) .chain(self.release_channel)
.chain(self.server)
.chain(self.project.iter().copied()) .chain(self.project.iter().copied())
} }
@ -162,6 +165,7 @@ pub struct SettingsStore {
setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>, setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
raw_default_settings: serde_json::Value, raw_default_settings: serde_json::Value,
raw_user_settings: serde_json::Value, raw_user_settings: serde_json::Value,
raw_server_settings: Option<serde_json::Value>,
raw_extension_settings: serde_json::Value, raw_extension_settings: serde_json::Value,
raw_local_settings: raw_local_settings:
BTreeMap<(WorktreeId, Arc<Path>), HashMap<LocalSettingsKind, serde_json::Value>>, BTreeMap<(WorktreeId, Arc<Path>), HashMap<LocalSettingsKind, serde_json::Value>>,
@ -219,6 +223,7 @@ impl SettingsStore {
setting_values: Default::default(), setting_values: Default::default(),
raw_default_settings: serde_json::json!({}), raw_default_settings: serde_json::json!({}),
raw_user_settings: serde_json::json!({}), raw_user_settings: serde_json::json!({}),
raw_server_settings: None,
raw_extension_settings: serde_json::json!({}), raw_extension_settings: serde_json::json!({}),
raw_local_settings: Default::default(), raw_local_settings: Default::default(),
tab_size_callback: Default::default(), tab_size_callback: Default::default(),
@ -269,6 +274,13 @@ impl SettingsStore {
.log_err(); .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 let extension_value = setting_value
.deserialize_setting(&self.raw_extension_settings) .deserialize_setting(&self.raw_extension_settings)
.log_err(); .log_err();
@ -277,9 +289,10 @@ impl SettingsStore {
.load_setting( .load_setting(
SettingsSources { SettingsSources {
default: &default_settings, default: &default_settings,
release_channel: release_channel_value.as_ref(),
extensions: extension_value.as_ref(), extensions: extension_value.as_ref(),
user: user_value.as_ref(), user: user_value.as_ref(),
release_channel: release_channel_value.as_ref(),
server: server_value.as_ref(),
project: &[], project: &[],
}, },
cx, cx,
@ -522,6 +535,29 @@ impl SettingsStore {
Ok(()) Ok(())
} }
pub fn set_server_settings(
&mut self,
server_settings_content: &str,
cx: &mut AppContext,
) -> Result<()> {
let settings: Option<serde_json::Value> = 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. /// Add or remove a set of local settings via a JSON string.
pub fn set_local_settings( pub fn set_local_settings(
&mut self, &mut self,
@ -530,8 +566,8 @@ impl SettingsStore {
kind: LocalSettingsKind, kind: LocalSettingsKind,
settings_content: Option<&str>, settings_content: Option<&str>,
cx: &mut AppContext, cx: &mut AppContext,
) -> Result<()> { ) -> std::result::Result<(), InvalidSettingsError> {
anyhow::ensure!( debug_assert!(
kind != LocalSettingsKind::Tasks, kind != LocalSettingsKind::Tasks,
"Attempted to submit tasks into the settings store" "Attempted to submit tasks into the settings store"
); );
@ -541,7 +577,13 @@ impl SettingsStore {
.entry((root_id, directory_path.clone())) .entry((root_id, directory_path.clone()))
.or_default(); .or_default();
let changed = if settings_content.is_some_and(|content| !content.is_empty()) { 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) { if Some(&new_contents) == raw_local_settings.get(&kind) {
false false
} else { } else {
@ -711,12 +753,16 @@ impl SettingsStore {
&mut self, &mut self,
changed_local_path: Option<(WorktreeId, &Path)>, changed_local_path: Option<(WorktreeId, &Path)>,
cx: &mut AppContext, cx: &mut AppContext,
) -> Result<()> { ) -> Result<(), InvalidSettingsError> {
// Reload the global and local values for every setting. // Reload the global and local values for every setting.
let mut project_settings_stack = Vec::<DeserializedSetting>::new(); let mut project_settings_stack = Vec::<DeserializedSetting>::new();
let mut paths_stack = Vec::<Option<(WorktreeId, &Path)>>::new(); let mut paths_stack = Vec::<Option<(WorktreeId, &Path)>>::new();
for setting_value in self.setting_values.values_mut() { 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 let extension_settings = setting_value
.deserialize_setting(&self.raw_extension_settings) .deserialize_setting(&self.raw_extension_settings)
@ -725,12 +771,17 @@ impl SettingsStore {
let user_settings = match setting_value.deserialize_setting(&self.raw_user_settings) { let user_settings = match setting_value.deserialize_setting(&self.raw_user_settings) {
Ok(settings) => Some(settings), Ok(settings) => Some(settings),
Err(error) => { Err(error) => {
return Err(anyhow!(InvalidSettingsError::UserSettings { return Err(InvalidSettingsError::UserSettings {
message: error.to_string() 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; let mut release_channel_settings = None;
if let Some(release_settings) = &self if let Some(release_settings) = &self
.raw_user_settings .raw_user_settings
@ -753,6 +804,7 @@ impl SettingsStore {
extensions: extension_settings.as_ref(), extensions: extension_settings.as_ref(),
user: user_settings.as_ref(), user: user_settings.as_ref(),
release_channel: release_channel_settings.as_ref(), release_channel: release_channel_settings.as_ref(),
server: server_settings.as_ref(),
project: &[], project: &[],
}, },
cx, cx,
@ -804,6 +856,7 @@ impl SettingsStore {
extensions: extension_settings.as_ref(), extensions: extension_settings.as_ref(),
user: user_settings.as_ref(), user: user_settings.as_ref(),
release_channel: release_channel_settings.as_ref(), release_channel: release_channel_settings.as_ref(),
server: server_settings.as_ref(),
project: &project_settings_stack.iter().collect::<Vec<_>>(), project: &project_settings_stack.iter().collect::<Vec<_>>(),
}, },
cx, cx,
@ -818,10 +871,10 @@ impl SettingsStore {
} }
} }
Err(error) => { Err(error) => {
return Err(anyhow!(InvalidSettingsError::LocalSettings { return Err(InvalidSettingsError::LocalSettings {
path: directory_path.join(local_settings_file_relative_path()), 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 { pub enum InvalidSettingsError {
LocalSettings { path: PathBuf, message: String }, LocalSettings { path: PathBuf, message: String },
UserSettings { message: String }, UserSettings { message: String },
ServerSettings { message: String },
DefaultSettings { message: String },
} }
impl std::fmt::Display for InvalidSettingsError { impl std::fmt::Display for InvalidSettingsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
InvalidSettingsError::LocalSettings { message, .. } InvalidSettingsError::LocalSettings { message, .. }
| InvalidSettingsError::UserSettings { message } => { | InvalidSettingsError::UserSettings { message }
| InvalidSettingsError::ServerSettings { message }
| InvalidSettingsError::DefaultSettings { message } => {
write!(f, "{}", message) write!(f, "{}", message)
} }
} }
@ -893,6 +950,9 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
release_channel: values release_channel: values
.release_channel .release_channel
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()), .map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
server: values
.server
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
project: values project: values
.project .project
.iter() .iter()

View file

@ -636,7 +636,12 @@ impl settings::Settings for ThemeSettings {
unnecessary_code_fade: defaults.unnecessary_code_fade.unwrap_or(0.0), 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 { if let Some(value) = value.ui_density {
this.ui_density = value; this.ui_density = value;
} }

View file

@ -1080,9 +1080,14 @@ impl Settings for VimModeSetting {
type FileContent = Option<bool>; type FileContent = Option<bool>;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> { fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
Ok(Self(sources.user.copied().flatten().unwrap_or( Ok(Self(
sources.default.ok_or_else(Self::missing_default)?, sources
))) .user
.or(sources.server)
.copied()
.flatten()
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?),
))
} }
} }

View file

@ -96,6 +96,9 @@ impl Settings for BaseKeymap {
if let Some(Some(user_value)) = sources.user.copied() { if let Some(Some(user_value)) = sources.user.copied() {
return Ok(user_value); 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) sources.default.ok_or_else(Self::missing_default)
} }
} }

View file

@ -16,30 +16,27 @@ pub fn init(cx: &mut AppContext) {
} }
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub struct NotificationId { pub enum NotificationId {
/// A [`TypeId`] used to uniquely identify this notification. Unique(TypeId),
type_id: TypeId, Composite(TypeId, ElementId),
/// A supplementary ID used to distinguish between multiple Named(SharedString),
/// notifications that have the same [`type_id`](Self::type_id);
id: Option<ElementId>,
} }
impl NotificationId { impl NotificationId {
/// Returns a unique [`NotificationId`] for the given type. /// Returns a unique [`NotificationId`] for the given type.
pub fn unique<T: 'static>() -> Self { pub fn unique<T: 'static>() -> Self {
Self { Self::Unique(TypeId::of::<T>())
type_id: TypeId::of::<T>(),
id: None,
}
} }
/// Returns a [`NotificationId`] for the given type that is also identified /// Returns a [`NotificationId`] for the given type that is also identified
/// by the provided ID. /// by the provided ID.
pub fn identified<T: 'static>(id: impl Into<ElementId>) -> Self { pub fn composite<T: 'static>(id: impl Into<ElementId>) -> Self {
Self { Self::Composite(TypeId::of::<T>(), id.into())
type_id: TypeId::of::<T>(), }
id: Some(id.into()),
} /// Builds a `NotificationId` out of the given string.
pub fn named(id: SharedString) -> Self {
Self::Named(id)
} }
} }

View file

@ -65,7 +65,7 @@ use release_channel::ReleaseChannel;
use remote::{SshClientDelegate, SshConnectionOptions}; use remote::{SshClientDelegate, SshConnectionOptions};
use serde::Deserialize; use serde::Deserialize;
use session::AppSession; use session::AppSession;
use settings::{InvalidSettingsError, Settings}; use settings::Settings;
use shared_screen::SharedScreen; use shared_screen::SharedScreen;
use sqlez::{ use sqlez::{
bindable::{Bind, Column, StaticColumnCount}, bindable::{Bind, Column, StaticColumnCount},
@ -839,31 +839,17 @@ impl Workspace {
} }
} }
project::Event::LocalSettingsUpdated(result) => { project::Event::Toast {
struct LocalSettingsUpdated; notification_id,
let id = NotificationId::unique::<LocalSettingsUpdated>(); message,
} => this.show_notification(
NotificationId::named(notification_id.clone()),
cx,
|cx| cx.new_view(|_| MessageNotification::new(message.clone())),
),
match result { project::Event::HideToast { notification_id } => {
Err(InvalidSettingsError::LocalSettings { message, path }) => { this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx)
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;
this.show_notification(
NotificationId::unique::<ProjectNotification>(),
cx,
|cx| cx.new_view(|_| MessageNotification::new(message.clone())),
)
} }
project::Event::LanguageServerPrompt(request) => { project::Event::LanguageServerPrompt(request) => {
@ -874,7 +860,7 @@ impl Workspace {
let id = hasher.finish(); let id = hasher.finish();
this.show_notification( this.show_notification(
NotificationId::identified::<LanguageServerPrompt>(id as usize), NotificationId::composite::<LanguageServerPrompt>(id as usize),
cx, cx,
|cx| { |cx| {
cx.new_view(|_| { cx.new_view(|_| {
@ -1808,6 +1794,7 @@ impl Workspace {
.flat_map(|pane| { .flat_map(|pane| {
pane.read(cx).items().filter_map(|item| { pane.read(cx).items().filter_map(|item| {
if item.is_dirty(cx) { if item.is_dirty(cx) {
item.tab_description(0, cx);
Some((pane.downgrade(), item.boxed_clone())) Some((pane.downgrade(), item.boxed_clone()))
} else { } else {
None None

View file

@ -27,13 +27,14 @@ use anyhow::Context as _;
use assets::Assets; use assets::Assets;
use futures::{channel::mpsc, select_biased, StreamExt}; use futures::{channel::mpsc, select_biased, StreamExt};
use outline_panel::OutlinePanel; use outline_panel::OutlinePanel;
use project::Item;
use project_panel::ProjectPanel; use project_panel::ProjectPanel;
use quick_action_bar::QuickActionBar; use quick_action_bar::QuickActionBar;
use release_channel::{AppCommitSha, ReleaseChannel}; use release_channel::{AppCommitSha, ReleaseChannel};
use rope::Rope; use rope::Rope;
use search::project_search::ProjectSearchBar; use search::project_search::ProjectSearchBar;
use settings::{ 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, DEFAULT_KEYMAP_PATH,
}; };
use std::any::TypeId; use std::any::TypeId;
@ -53,7 +54,9 @@ use workspace::{
open_new, AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings, open_new, AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings,
}; };
use workspace::{notifications::DetachAndPromptErr, Pane}; use workspace::{notifications::DetachAndPromptErr, Pane};
use zed_actions::{OpenAccountSettings, OpenBrowser, OpenSettings, OpenZedUrl, Quit}; use zed_actions::{
OpenAccountSettings, OpenBrowser, OpenServerSettings, OpenSettings, OpenZedUrl, Quit,
};
actions!( actions!(
zed, zed,
@ -64,8 +67,8 @@ actions!(
Minimize, Minimize,
OpenDefaultKeymap, OpenDefaultKeymap,
OpenDefaultSettings, OpenDefaultSettings,
OpenLocalSettings, OpenProjectSettings,
OpenLocalTasks, OpenProjectTasks,
OpenTasks, OpenTasks,
ResetDatabase, ResetDatabase,
ShowAll, ShowAll,
@ -218,6 +221,7 @@ pub fn initialize_workspace(
let handle = cx.view().downgrade(); let handle = cx.view().downgrade();
cx.on_window_should_close(move |cx| { cx.on_window_should_close(move |cx| {
handle handle
.update(cx, |workspace, cx| { .update(cx, |workspace, cx| {
// We'll handle closing asynchronously // We'll handle closing asynchronously
@ -428,8 +432,8 @@ pub fn initialize_workspace(
); );
}, },
) )
.register_action(open_local_settings_file) .register_action(open_project_settings_file)
.register_action(open_local_tasks_file) .register_action(open_project_tasks_file)
.register_action( .register_action(
move |workspace: &mut Workspace, move |workspace: &mut Workspace,
_: &OpenDefaultKeymap, _: &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); 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, workspace: &mut Workspace,
_: &OpenLocalSettings, _: &OpenProjectSettings,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) { ) {
open_local_file( open_local_file(
workspace, workspace,
local_settings_file_relative_path(), local_settings_file_relative_path(),
initial_local_settings_content(), initial_project_settings_content(),
cx, cx,
) )
} }
fn open_local_tasks_file( fn open_project_tasks_file(
workspace: &mut Workspace, workspace: &mut Workspace,
_: &OpenLocalTasks, _: &OpenProjectTasks,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) { ) {
open_local_file( open_local_file(

View file

@ -19,7 +19,7 @@ pub fn app_menus() -> Vec<Menu> {
MenuItem::action("Open Key Bindings", zed_actions::OpenKeymap), MenuItem::action("Open Key Bindings", zed_actions::OpenKeymap),
MenuItem::action("Open Default Settings", super::OpenDefaultSettings), MenuItem::action("Open Default Settings", super::OpenDefaultSettings),
MenuItem::action("Open Default Key Bindings", super::OpenDefaultKeymap), 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()), MenuItem::action("Select Theme...", theme_selector::Toggle::default()),
], ],
}), }),

View file

@ -27,6 +27,7 @@ actions!(
[ [
OpenSettings, OpenSettings,
OpenAccountSettings, OpenAccountSettings,
OpenServerSettings,
Quit, Quit,
OpenKeymap, OpenKeymap,
About, About,

View file

@ -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. 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. 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.