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
co-authored by Conrad Richard
parent 1dda039f38
commit f944ebc4cb
44 changed files with 804 additions and 218 deletions
+1 -1
View File
@@ -2278,7 +2278,7 @@ impl InlineAssist {
struct InlineAssistantError;
let id =
NotificationId::identified::<InlineAssistantError>(
NotificationId::composite::<InlineAssistantError>(
assist_id.0,
);
@@ -38,7 +38,10 @@ impl Settings for SlashCommandSettings {
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut AppContext) -> Result<Self> {
SettingsSources::<Self::FileContent>::json_merge_with(
[sources.default].into_iter().chain(sources.user),
[sources.default]
.into_iter()
.chain(sources.user)
.chain(sources.server),
)
}
}
@@ -414,7 +414,7 @@ impl TerminalInlineAssist {
struct InlineAssistantError;
let id =
NotificationId::identified::<InlineAssistantError>(
NotificationId::composite::<InlineAssistantError>(
assist_id.0,
);
+1 -1
View File
@@ -130,7 +130,7 @@ impl Settings for AutoUpdateSetting {
type FileContent = Option<AutoUpdateSettingContent>;
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()
.find_map(|value| value.copied().flatten())
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?);
+13 -6
View File
@@ -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<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
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)?),
})
@@ -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)
+9 -4
View File
@@ -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| {
+4 -1
View File
@@ -42,7 +42,10 @@ impl Settings for ExtensionSettings {
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut AppContext) -> Result<Self> {
SettingsSources::<Self::FileContent>::json_merge_with(
[sources.default].into_iter().chain(sources.user),
[sources.default]
.into_iter()
.chain(sources.user)
.chain(sources.server),
)
}
}
@@ -163,7 +163,7 @@ pub(crate) fn suggest(buffer: Model<Buffer>, cx: &mut ViewContext<Workspace>) {
struct ExtensionSuggestionNotification;
let notification_id = NotificationId::identified::<ExtensionSuggestionNotification>(
let notification_id = NotificationId::composite::<ExtensionSuggestionNotification>(
SharedString::from(extension_id.clone()),
);
+6
View File
@@ -567,6 +567,12 @@ pub struct WeakModel<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> Sync for WeakModel<T> {}
+6
View File
@@ -317,6 +317,12 @@ pub fn read_proxy_from_env() -> Option<Uri> {
pub struct BlockedHttpClient;
impl BlockedHttpClient {
pub fn new() -> Self {
BlockedHttpClient
}
}
impl HttpClient for BlockedHttpClient {
fn send(
&self,
+1 -1
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 {
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;
+11
View File
@@ -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<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.
pub fn log_file() -> &'static PathBuf {
static LOG_FILE: OnceLock<PathBuf> = OnceLock::new();
+4 -7
View File
@@ -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<Arc<dyn HttpClient>>,
http_client: Arc<dyn HttpClient>,
environment: Model<ProjectEnvironment>,
fs: Arc<dyn Fs>,
yarn: Model<YarnPathStore>,
@@ -839,7 +839,7 @@ impl LspStore {
prettier_store: Model<PrettierStore>,
environment: Model<ProjectEnvironment>,
languages: Arc<LanguageRegistry>,
http_client: Option<Arc<dyn HttpClient>>,
http_client: Arc<dyn HttpClient>,
fs: Arc<dyn Fs>,
cx: &mut ModelContext<Self>,
) -> 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)
}
+98 -26
View File
@@ -222,8 +222,13 @@ pub enum Event {
LanguageServerAdded(LanguageServerId, LanguageServerName, Option<WorktreeId>),
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<Buffer>),
ActiveEntryChanged(Option<ProjectEntryId>),
@@ -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<Self>,
) {
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<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(
&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<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())) })
}
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<Path>,
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
async fn handle_update_worktree(
this: Model<Self>,
@@ -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()
+33 -34
View File
@@ -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::<SettingsStore, anyhow::Result<()>>(|store, cx| {
store.set_local_settings(
.update_global::<SettingsStore, _>(|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::<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(())));
}
}
}
}
}
+4 -3
View File
@@ -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 {
+5 -1
View File
@@ -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}")
+17 -1
View File
@@ -153,6 +153,22 @@ impl WorktreeStore {
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>(
&'a self,
entry_id: ProjectEntryId,
@@ -957,7 +973,7 @@ impl WorktreeStore {
}
}
#[derive(Clone)]
#[derive(Clone, Debug)]
enum WorktreeHandle {
Strong(Model<Worktree>),
Weak(WeakModel<Worktree>),
+4 -3
View File
@@ -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 {
+21 -1
View File
@@ -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;
}
+9 -1
View File
@@ -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!(
+1
View File
@@ -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"] }
+2 -2
View File
@@ -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::<SshServerRemoval>(
NotificationId::composite::<SshServerRemoval>(
connection_string.clone(),
),
notification,
+13 -3
View File
@@ -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<PathBuf> {
fn remote_server_binary_path(
&self,
platform: SshPlatform,
cx: &mut AsyncAppContext,
) -> Result<PathBuf> {
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");
+7 -3
View File
@@ -137,7 +137,11 @@ pub trait SshClientDelegate: Send + Sync {
prompt: String,
cx: &mut AsyncAppContext,
) -> 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(
&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()
}
+5 -1
View File
@@ -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"]
+85 -6
View File
@@ -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<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 {
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<ChannelClient>, fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> 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>,
) -> 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<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(
this: Model<Self>,
envelope: TypedEnvelope<proto::FindSearchCandidates>,
@@ -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
+1 -1
View File
@@ -6,4 +6,4 @@ pub mod unix;
#[cfg(test)]
mod remote_editing_tests;
pub use headless_project::HeadlessProject;
pub use headless_project::{HeadlessAppState, HeadlessProject};
+193 -26
View File
@@ -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<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 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<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<()> {
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<S: AsyncWrite + Unpin>(
stream.write_all(buffer).await?;
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
}
+5 -1
View File
@@ -88,7 +88,11 @@ pub fn initial_user_settings_content() -> Cow<'static, str> {
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")
}
+73 -13
View File
@@ -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<TypeId, Box<dyn AnySettingValue>>,
raw_default_settings: serde_json::Value,
raw_user_settings: serde_json::Value,
raw_server_settings: Option<serde_json::Value>,
raw_extension_settings: serde_json::Value,
raw_local_settings:
BTreeMap<(WorktreeId, Arc<Path>), HashMap<LocalSettingsKind, serde_json::Value>>,
@@ -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<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.
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::<DeserializedSetting>::new();
let mut paths_stack = Vec::<Option<(WorktreeId, &Path)>>::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::<Vec<_>>(),
},
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<T: Settings> AnySettingValue for SettingValue<T> {
release_channel: values
.release_channel
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
server: values
.server
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
project: values
.project
.iter()
+6 -1
View File
@@ -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;
}
+8 -3
View File
@@ -1080,9 +1080,14 @@ impl Settings for VimModeSetting {
type FileContent = Option<bool>;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
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)?),
))
}
}
@@ -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)
}
}
+12 -15
View File
@@ -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<ElementId>,
pub enum NotificationId {
Unique(TypeId),
Composite(TypeId, ElementId),
Named(SharedString),
}
impl NotificationId {
/// Returns a unique [`NotificationId`] for the given type.
pub fn unique<T: 'static>() -> Self {
Self {
type_id: TypeId::of::<T>(),
id: None,
}
Self::Unique(TypeId::of::<T>())
}
/// Returns a [`NotificationId`] for the given type that is also identified
/// by the provided ID.
pub fn identified<T: 'static>(id: impl Into<ElementId>) -> Self {
Self {
type_id: TypeId::of::<T>(),
id: Some(id.into()),
}
pub fn composite<T: 'static>(id: impl Into<ElementId>) -> Self {
Self::Composite(TypeId::of::<T>(), id.into())
}
/// Builds a `NotificationId` out of the given string.
pub fn named(id: SharedString) -> Self {
Self::Named(id)
}
}
+13 -26
View File
@@ -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::<LocalSettingsUpdated>();
project::Event::Toast {
notification_id,
message,
} => this.show_notification(
NotificationId::named(notification_id.clone()),
cx,
|cx| cx.new_view(|_| MessageNotification::new(message.clone())),
),
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;
this.show_notification(
NotificationId::unique::<ProjectNotification>(),
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::<LanguageServerPrompt>(id as usize),
NotificationId::composite::<LanguageServerPrompt>(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
+34 -11
View File
@@ -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<Workspace>,
) {
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<Workspace>,
) {
open_local_file(
+1 -1
View File
@@ -19,7 +19,7 @@ pub fn app_menus() -> Vec<Menu> {
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()),
],
}),
+1
View File
@@ -27,6 +27,7 @@ actions!(
[
OpenSettings,
OpenAccountSettings,
OpenServerSettings,
Quit,
OpenKeymap,
About,