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:
co-authored by
Conrad
Richard
parent
1dda039f38
commit
f944ebc4cb
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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)?);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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()),
|
||||
);
|
||||
|
||||
|
||||
@@ -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> {}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(())));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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>),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,4 +6,4 @@ pub mod unix;
|
||||
#[cfg(test)]
|
||||
mod remote_editing_tests;
|
||||
|
||||
pub use headless_project::HeadlessProject;
|
||||
pub use headless_project::{HeadlessAppState, HeadlessProject};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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()),
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -27,6 +27,7 @@ actions!(
|
||||
[
|
||||
OpenSettings,
|
||||
OpenAccountSettings,
|
||||
OpenServerSettings,
|
||||
Quit,
|
||||
OpenKeymap,
|
||||
About,
|
||||
|
||||
Reference in New Issue
Block a user