Rework remote task synchronization (#18746)

Reworks the way tasks are stored, accessed and synchronized in the
`project`.
Now both collab and ssh remote projects use the same TaskStorage kind to
get the task context from the remote host, and worktree task templates
are synchronized along with other worktree settings.

Release Notes:

- Adds ssh support to tasks, improves collab-remote projects' tasks sync
This commit is contained in:
Kirill Bulatov 2024-10-09 22:28:42 +03:00 committed by GitHub
parent f1053ff525
commit 49c75eb062
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1262 additions and 1366 deletions

View file

@ -3,20 +3,30 @@ use collections::HashMap;
use fs::Fs;
use gpui::{AppContext, AsyncAppContext, BorrowAppContext, EventEmitter, Model, ModelContext};
use language::LanguageServerName;
use paths::local_settings_file_relative_path;
use paths::{
local_settings_file_relative_path, local_tasks_file_relative_path,
local_vscode_tasks_file_relative_path,
};
use rpc::{proto, AnyProtoClient, TypedEnvelope};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{InvalidSettingsError, LocalSettingsKind, Settings, SettingsSources, SettingsStore};
use settings::{
parse_json_with_comments, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation,
SettingsSources, SettingsStore,
};
use std::{
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use task::{TaskTemplates, VsCodeTaskFile};
use util::ResultExt;
use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
use crate::{
task_store::TaskStore,
worktree_store::{WorktreeStore, WorktreeStoreEvent},
};
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct ProjectSettings {
@ -202,12 +212,13 @@ pub struct SettingsObserver {
downstream_client: Option<AnyProtoClient>,
worktree_store: Model<WorktreeStore>,
project_id: u64,
task_store: Model<TaskStore>,
}
/// SettingsObserver observers changes to .zed/settings.json files in local worktrees
/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
/// (or the equivalent protobuf messages from upstream) and updates local settings
/// and sends notifications downstream.
/// In ssh mode it also monitors ~/.config/zed/settings.json and sends the content
/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
/// upstream.
impl SettingsObserver {
pub fn init(client: &AnyProtoClient) {
@ -218,6 +229,7 @@ impl SettingsObserver {
pub fn new_local(
fs: Arc<dyn Fs>,
worktree_store: Model<WorktreeStore>,
task_store: Model<TaskStore>,
cx: &mut ModelContext<Self>,
) -> Self {
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
@ -225,6 +237,7 @@ impl SettingsObserver {
Self {
worktree_store,
task_store,
mode: SettingsObserverMode::Local(fs),
downstream_client: None,
project_id: 0,
@ -234,10 +247,12 @@ impl SettingsObserver {
pub fn new_ssh(
client: AnyProtoClient,
worktree_store: Model<WorktreeStore>,
task_store: Model<TaskStore>,
cx: &mut ModelContext<Self>,
) -> Self {
let this = Self {
worktree_store,
task_store,
mode: SettingsObserverMode::Ssh(client.clone()),
downstream_client: None,
project_id: 0,
@ -246,9 +261,14 @@ impl SettingsObserver {
this
}
pub fn new_remote(worktree_store: Model<WorktreeStore>, _: &mut ModelContext<Self>) -> Self {
pub fn new_remote(
worktree_store: Model<WorktreeStore>,
task_store: Model<TaskStore>,
_: &mut ModelContext<Self>,
) -> Self {
Self {
worktree_store,
task_store,
mode: SettingsObserverMode::Remote,
downstream_client: None,
project_id: 0,
@ -319,19 +339,32 @@ impl SettingsObserver {
}
pub async fn handle_update_user_settings(
_: Model<Self>,
settings_observer: Model<Self>,
envelope: TypedEnvelope<proto::UpdateUserSettings>,
cx: AsyncAppContext,
mut cx: AsyncAppContext,
) -> anyhow::Result<()> {
cx.update_global(move |settings_store: &mut SettingsStore, cx| {
settings_store.set_user_settings(&envelope.payload.content, cx)
})??;
match envelope.payload.kind() {
proto::update_user_settings::Kind::Settings => {
cx.update_global(move |settings_store: &mut SettingsStore, cx| {
settings_store.set_user_settings(&envelope.payload.content, cx)
})
}
proto::update_user_settings::Kind::Tasks => {
settings_observer.update(&mut cx, |settings_observer, cx| {
settings_observer.task_store.update(cx, |task_store, cx| {
task_store.update_user_tasks(None, Some(&envelope.payload.content), cx)
})
})
}
}??;
Ok(())
}
pub fn maintain_ssh_settings(&self, ssh: AnyProtoClient, cx: &mut ModelContext<Self>) {
let mut settings = cx.global::<SettingsStore>().raw_user_settings().clone();
let settings_store = cx.global::<SettingsStore>();
let mut settings = settings_store.raw_user_settings().clone();
if let Some(content) = serde_json::to_string(&settings).log_err() {
ssh.send(proto::UpdateUserSettings {
project_id: 0,
@ -389,7 +422,43 @@ impl SettingsObserver {
let mut settings_contents = Vec::new();
for (path, _, change) in changes.iter() {
let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
let settings_dir = Arc::<Path>::from(
path.ancestors()
.nth(local_settings_file_relative_path().components().count())
.unwrap(),
);
(settings_dir, LocalSettingsKind::Settings)
} else if path.ends_with(local_tasks_file_relative_path()) {
let settings_dir = Arc::<Path>::from(
path.ancestors()
.nth(
local_tasks_file_relative_path()
.components()
.count()
.saturating_sub(1),
)
.unwrap(),
);
(settings_dir, LocalSettingsKind::Tasks)
} else if path.ends_with(local_vscode_tasks_file_relative_path()) {
let settings_dir = Arc::<Path>::from(
path.ancestors()
.nth(
local_vscode_tasks_file_relative_path()
.components()
.count()
.saturating_sub(1),
)
.unwrap(),
);
(settings_dir, LocalSettingsKind::Tasks)
} else {
continue;
};
let removed = change == &PathChange::Removed;
let fs = fs.clone();
let abs_path = match worktree.read(cx).absolutize(path) {
Ok(abs_path) => abs_path,
Err(e) => {
@ -397,26 +466,42 @@ impl SettingsObserver {
continue;
}
};
if path.ends_with(local_settings_file_relative_path()) {
let settings_dir = Arc::from(
path.ancestors()
.nth(local_settings_file_relative_path().components().count())
.unwrap(),
);
let fs = fs.clone();
settings_contents.push(async move {
(
settings_dir,
LocalSettingsKind::Settings,
if removed {
None
} else {
Some(async move { fs.load(&abs_path).await }.await)
},
)
});
}
settings_contents.push(async move {
(
settings_dir,
kind,
if removed {
None
} else {
Some(
async move {
let content = fs.load(&abs_path).await?;
if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
let vscode_tasks =
parse_json_with_comments::<VsCodeTaskFile>(&content)
.with_context(|| {
format!("parsing VSCode tasks, file {abs_path:?}")
})?;
let zed_tasks = TaskTemplates::try_from(vscode_tasks)
.with_context(|| {
format!(
"converting VSCode tasks into Zed ones, file {abs_path:?}"
)
})?;
serde_json::to_string(&zed_tasks).with_context(|| {
format!(
"serializing Zed tasks into JSON, file {abs_path:?}"
)
})
} else {
Ok(content)
}
}
.await,
)
},
)
});
}
if settings_contents.is_empty() {
@ -450,47 +535,64 @@ impl SettingsObserver {
) {
let worktree_id = worktree.read(cx).id();
let remote_worktree_id = worktree.read(cx).id();
let task_store = self.task_store.clone();
let result = cx.update_global::<SettingsStore, anyhow::Result<()>>(|store, cx| {
for (directory, kind, file_content) in settings_contents {
store.set_local_settings(
worktree_id,
directory.clone(),
kind,
file_content.as_deref(),
cx,
)?;
for (directory, kind, file_content) in settings_contents {
let result = match kind {
LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
.update_global::<SettingsStore, anyhow::Result<()>>(|store, cx| {
store.set_local_settings(
worktree_id,
directory.clone(),
kind,
file_content.as_deref(),
cx,
)
}),
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,
)
}),
};
if let Some(downstream_client) = &self.downstream_client {
downstream_client
.send(proto::UpdateWorktreeSettings {
project_id: self.project_id,
worktree_id: remote_worktree_id.to_proto(),
path: directory.to_string_lossy().into_owned(),
content: file_content,
kind: Some(local_settings_kind_to_proto(kind).into()),
})
.log_err();
}
if let Some(downstream_client) = &self.downstream_client {
downstream_client
.send(proto::UpdateWorktreeSettings {
project_id: self.project_id,
worktree_id: remote_worktree_id.to_proto(),
path: directory.to_string_lossy().into_owned(),
content: file_content,
kind: Some(local_settings_kind_to_proto(kind).into()),
})
.log_err();
}
anyhow::Ok(())
});
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)));
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(())));
Ok(()) => {
cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(())));
}
}
}
}