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:
parent
1dda039f38
commit
f944ebc4cb
44 changed files with 804 additions and 218 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -8977,6 +8977,7 @@ dependencies = [
|
|||
"log",
|
||||
"menu",
|
||||
"ordered-float 2.10.1",
|
||||
"paths",
|
||||
"picker",
|
||||
"project",
|
||||
"release_channel",
|
||||
|
@ -9136,6 +9137,7 @@ name = "remote_server"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-watch",
|
||||
"backtrace",
|
||||
"cargo_toml",
|
||||
"clap",
|
||||
|
@ -9151,8 +9153,10 @@ dependencies = [
|
|||
"log",
|
||||
"lsp",
|
||||
"node_runtime",
|
||||
"paths",
|
||||
"project",
|
||||
"remote",
|
||||
"reqwest_client",
|
||||
"rpc",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
|
|
7
assets/settings/initial_server_settings.json
Normal file
7
assets/settings/initial_server_settings.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
// Server-specific settings
|
||||
//
|
||||
// For a full list of overridable settings, and general information on settings,
|
||||
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
|
||||
{
|
||||
"lsp": {}
|
||||
}
|
|
@ -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,7 +473,12 @@ 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(
|
||||
diagnostics: sources
|
||||
.user
|
||||
.as_ref()
|
||||
.or(sources.server.as_ref())
|
||||
.and_then(|v| v.diagnostics)
|
||||
.unwrap_or(
|
||||
sources
|
||||
.default
|
||||
.diagnostics
|
||||
|
@ -481,6 +487,7 @@ impl settings::Settings for TelemetrySettings {
|
|||
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,19 +538,39 @@ 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(
|
||||
task_store
|
||||
.update_user_tasks(
|
||||
Some(SettingsLocation {
|
||||
worktree_id,
|
||||
path: directory.as_ref(),
|
||||
|
@ -558,6 +578,7 @@ impl SettingsObserver {
|
|||
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>();
|
||||
|
||||
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>(),
|
||||
project::Event::Toast {
|
||||
notification_id,
|
||||
message,
|
||||
} => this.show_notification(
|
||||
NotificationId::named(notification_id.clone()),
|
||||
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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -15,7 +15,7 @@ TBD: Add settings documentation about how settings are merged as overlays. E.g.
|
|||
|
||||
Your settings file can be opened with {#kb zed::OpenSettings}. By default it is located at `~/.config/zed/settings.json`, though if you have XDG_CONFIG_HOME in your environment on Linux it will be at `$XDG_CONFIG_HOME/zed/settings.json` instead.
|
||||
|
||||
This configuration is merged with any local configuration inside your projects. You can open the project settings by running {#action zed::OpenLocalSettings} from the command palette. This will create a `.zed` directory containing`.zed/settings.json`.
|
||||
This configuration is merged with any local configuration inside your projects. You can open the project settings by running {#action zed::OpenProjectSettings} from the command palette. This will create a `.zed` directory containing`.zed/settings.json`.
|
||||
|
||||
Although most projects will only need one settings file at the root, you can add more local settings files for subdirectories as needed. Not all settings can be set in local files, just those that impact the behavior of the editor and language tooling. For example you can set `tab_size`, `formatter` etc. but not `theme`, `vim_mode` and similar.
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue