Replicate project-specific settings when collaborating

This commit is contained in:
Max Brunsfeld 2023-05-30 11:05:08 -07:00
parent ed0fa2404c
commit 8f95435548
10 changed files with 432 additions and 18 deletions

View File

@ -112,6 +112,16 @@ CREATE INDEX "index_worktree_repository_statuses_on_project_id" ON "worktree_rep
CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id_and_work_directory_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");
CREATE TABLE "worktree_settings_files" (
"project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL,
"path" VARCHAR NOT NULL,
"content" TEXT,
PRIMARY KEY(project_id, worktree_id, path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_settings_files_on_project_id" ON "worktree_settings_files" ("project_id");
CREATE INDEX "index_worktree_settings_files_on_project_id_and_worktree_id" ON "worktree_settings_files" ("project_id", "worktree_id");
CREATE TABLE "worktree_diagnostic_summaries" (
"project_id" INTEGER NOT NULL,

View File

@ -0,0 +1,10 @@
CREATE TABLE "worktree_settings_files" (
"project_id" INTEGER NOT NULL,
"worktree_id" INT8 NOT NULL,
"path" VARCHAR NOT NULL,
"content" TEXT NOT NULL,
PRIMARY KEY(project_id, worktree_id, path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_settings_files_on_project_id" ON "worktree_settings_files" ("project_id");
CREATE INDEX "index_settings_files_on_project_id_and_wt_id" ON "worktree_settings_files" ("project_id", "worktree_id");

View File

@ -16,6 +16,7 @@ mod worktree_diagnostic_summary;
mod worktree_entry;
mod worktree_repository;
mod worktree_repository_statuses;
mod worktree_settings_file;
use crate::executor::Executor;
use crate::{Error, Result};
@ -1494,6 +1495,7 @@ impl Database {
updated_repositories: Default::default(),
removed_repositories: Default::default(),
diagnostic_summaries: Default::default(),
settings_files: Default::default(),
scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64,
};
@ -1638,6 +1640,25 @@ impl Database {
})
.collect::<Vec<_>>();
{
let mut db_settings_files = worktree_settings_file::Entity::find()
.filter(worktree_settings_file::Column::ProjectId.eq(project_id))
.stream(&*tx)
.await?;
while let Some(db_settings_file) = db_settings_files.next().await {
let db_settings_file = db_settings_file?;
if let Some(worktree) = worktrees
.iter_mut()
.find(|w| w.id == db_settings_file.worktree_id as u64)
{
worktree.settings_files.push(WorktreeSettingsFile {
path: db_settings_file.path,
content: db_settings_file.content,
});
}
}
}
let mut collaborators = project
.find_related(project_collaborator::Entity)
.all(&*tx)
@ -2637,6 +2658,58 @@ impl Database {
.await
}
pub async fn update_worktree_settings(
&self,
update: &proto::UpdateWorktreeSettings,
connection: ConnectionId,
) -> Result<RoomGuard<Vec<ConnectionId>>> {
let project_id = ProjectId::from_proto(update.project_id);
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
// Ensure the update comes from the host.
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
if project.host_connection()? != connection {
return Err(anyhow!("can't update a project hosted by someone else"))?;
}
if let Some(content) = &update.content {
worktree_settings_file::Entity::insert(worktree_settings_file::ActiveModel {
project_id: ActiveValue::Set(project_id),
worktree_id: ActiveValue::Set(update.worktree_id as i64),
path: ActiveValue::Set(update.path.clone()),
content: ActiveValue::Set(content.clone()),
})
.on_conflict(
OnConflict::columns([
worktree_settings_file::Column::ProjectId,
worktree_settings_file::Column::WorktreeId,
worktree_settings_file::Column::Path,
])
.update_column(worktree_settings_file::Column::Content)
.to_owned(),
)
.exec(&*tx)
.await?;
} else {
worktree_settings_file::Entity::delete(worktree_settings_file::ActiveModel {
project_id: ActiveValue::Set(project_id),
worktree_id: ActiveValue::Set(update.worktree_id as i64),
path: ActiveValue::Set(update.path.clone()),
..Default::default()
})
.exec(&*tx)
.await?;
}
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
Ok(connection_ids)
})
.await
}
pub async fn join_project(
&self,
project_id: ProjectId,
@ -2707,6 +2780,7 @@ impl Database {
entries: Default::default(),
repository_entries: Default::default(),
diagnostic_summaries: Default::default(),
settings_files: Default::default(),
scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64,
},
@ -2819,6 +2893,25 @@ impl Database {
}
}
// Populate worktree settings files
{
let mut db_settings_files = worktree_settings_file::Entity::find()
.filter(worktree_settings_file::Column::ProjectId.eq(project_id))
.stream(&*tx)
.await?;
while let Some(db_settings_file) = db_settings_files.next().await {
let db_settings_file = db_settings_file?;
if let Some(worktree) =
worktrees.get_mut(&(db_settings_file.worktree_id as u64))
{
worktree.settings_files.push(WorktreeSettingsFile {
path: db_settings_file.path,
content: db_settings_file.content,
});
}
}
}
// Populate language servers.
let language_servers = project
.find_related(language_server::Entity)
@ -3482,6 +3575,7 @@ pub struct RejoinedWorktree {
pub updated_repositories: Vec<proto::RepositoryEntry>,
pub removed_repositories: Vec<u64>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub settings_files: Vec<WorktreeSettingsFile>,
pub scan_id: u64,
pub completed_scan_id: u64,
}
@ -3537,10 +3631,17 @@ pub struct Worktree {
pub entries: Vec<proto::Entry>,
pub repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub settings_files: Vec<WorktreeSettingsFile>,
pub scan_id: u64,
pub completed_scan_id: u64,
}
#[derive(Debug)]
pub struct WorktreeSettingsFile {
pub path: String,
pub content: String,
}
#[cfg(test)]
pub use test::*;

View File

@ -0,0 +1,19 @@
use super::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "worktree_settings_files")]
pub struct Model {
#[sea_orm(primary_key)]
pub project_id: ProjectId,
#[sea_orm(primary_key)]
pub worktree_id: i64,
#[sea_orm(primary_key)]
pub path: String,
pub content: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -200,6 +200,7 @@ impl Server {
.add_message_handler(start_language_server)
.add_message_handler(update_language_server)
.add_message_handler(update_diagnostic_summary)
.add_message_handler(update_worktree_settings)
.add_request_handler(forward_project_request::<proto::GetHover>)
.add_request_handler(forward_project_request::<proto::GetDefinition>)
.add_request_handler(forward_project_request::<proto::GetTypeDefinition>)
@ -1088,6 +1089,18 @@ async fn rejoin_room(
},
)?;
}
for settings_file in worktree.settings_files {
session.peer.send(
session.connection_id,
proto::UpdateWorktreeSettings {
project_id: project.id.to_proto(),
worktree_id: worktree.id,
path: settings_file.path,
content: Some(settings_file.content),
},
)?;
}
}
for language_server in &project.language_servers {
@ -1410,6 +1423,18 @@ async fn join_project(
},
)?;
}
for settings_file in dbg!(worktree.settings_files) {
session.peer.send(
session.connection_id,
proto::UpdateWorktreeSettings {
project_id: project_id.to_proto(),
worktree_id: worktree.id,
path: settings_file.path,
content: Some(settings_file.content),
},
)?;
}
}
for language_server in &project.language_servers {
@ -1525,6 +1550,31 @@ async fn update_diagnostic_summary(
Ok(())
}
async fn update_worktree_settings(
message: proto::UpdateWorktreeSettings,
session: Session,
) -> Result<()> {
dbg!(&message);
let guest_connection_ids = session
.db()
.await
.update_worktree_settings(&message, session.connection_id)
.await?;
broadcast(
Some(session.connection_id),
guest_connection_ids.iter().copied(),
|connection_id| {
session
.peer
.forward_send(session.connection_id, connection_id, message.clone())
},
);
Ok(())
}
async fn start_language_server(
request: proto::StartLanguageServer,
session: Session,

View File

@ -3114,6 +3114,135 @@ async fn test_fs_operations(
});
}
#[gpui::test(iterations = 10)]
async fn test_local_settings(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
// As client A, open a project that contains some local settings files
client_a
.fs
.insert_tree(
"/dir",
json!({
".zed": {
"settings.json": r#"{ "tab_size": 2 }"#
},
"a": {
".zed": {
"settings.json": r#"{ "tab_size": 8 }"#
},
"a.txt": "a-contents",
},
"b": {
"b.txt": "b-contents",
}
}),
)
.await;
let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
// As client B, join that project and observe the local settings.
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
deterministic.run_until_parked();
cx_b.read(|cx| {
let store = cx.global::<SettingsStore>();
assert_eq!(
store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
&[
(Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
]
)
});
// As client A, update a settings file. As Client B, see the changed settings.
client_a
.fs
.insert_file("/dir/.zed/settings.json", r#"{}"#.into())
.await;
deterministic.run_until_parked();
cx_b.read(|cx| {
let store = cx.global::<SettingsStore>();
assert_eq!(
store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
&[
(Path::new("").into(), r#"{}"#.to_string()),
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
]
)
});
// As client A, create and remove some settings files. As client B, see the changed settings.
client_a
.fs
.remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
.await
.unwrap();
client_a
.fs
.create_dir("/dir/b/.zed".as_ref())
.await
.unwrap();
client_a
.fs
.insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
.await;
deterministic.run_until_parked();
cx_b.read(|cx| {
let store = cx.global::<SettingsStore>();
assert_eq!(
store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
&[
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
(Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
]
)
});
// As client B, disconnect.
server.forbid_connections();
server.disconnect_client(client_b.peer_id().unwrap());
// As client A, change and remove settings files while client B is disconnected.
client_a
.fs
.insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
.await;
client_a
.fs
.remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
.await
.unwrap();
deterministic.run_until_parked();
// As client B, reconnect and see the changed settings.
server.allow_connections();
deterministic.advance_clock(RECEIVE_TIMEOUT);
cx_b.read(|cx| {
let store = cx.global::<SettingsStore>();
assert_eq!(
store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
&[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
)
});
}
#[gpui::test(iterations = 10)]
async fn test_buffer_conflict_after_save(
deterministic: Arc<Deterministic>,

View File

@ -462,6 +462,7 @@ impl Project {
client.add_model_request_handler(Self::handle_update_buffer);
client.add_model_message_handler(Self::handle_update_diagnostic_summary);
client.add_model_message_handler(Self::handle_update_worktree);
client.add_model_message_handler(Self::handle_update_worktree_settings);
client.add_model_request_handler(Self::handle_create_project_entry);
client.add_model_request_handler(Self::handle_rename_project_entry);
client.add_model_request_handler(Self::handle_copy_project_entry);
@ -1105,6 +1106,21 @@ impl Project {
.log_err();
}
let store = cx.global::<SettingsStore>();
for worktree in self.worktrees(cx) {
let worktree_id = worktree.read(cx).id().to_proto();
for (path, content) in store.local_settings(worktree.id()) {
self.client
.send(proto::UpdateWorktreeSettings {
project_id,
worktree_id,
path: path.to_string_lossy().into(),
content: Some(content),
})
.log_err();
}
}
let (updates_tx, mut updates_rx) = mpsc::unbounded();
let client = self.client.clone();
self.client_state = Some(ProjectClientState::Local {
@ -1217,6 +1233,14 @@ impl Project {
message_id: u32,
cx: &mut ModelContext<Self>,
) -> Result<()> {
cx.update_global::<SettingsStore, _, _>(|store, cx| {
for worktree in &self.worktrees {
store
.clear_local_settings(worktree.handle_id(), cx)
.log_err();
}
});
self.join_project_response_message_id = message_id;
self.set_worktrees_from_proto(message.worktrees, cx)?;
self.set_collaborators_from_proto(message.collaborators, cx)?;
@ -4888,8 +4912,12 @@ impl Project {
.push(WorktreeHandle::Weak(worktree.downgrade()));
}
cx.observe_release(worktree, |this, worktree, cx| {
let handle_id = worktree.id();
cx.observe_release(worktree, move |this, worktree, cx| {
let _ = this.remove_worktree(worktree.id(), cx);
cx.update_global::<SettingsStore, _, _>(|store, cx| {
store.clear_local_settings(handle_id, cx).log_err()
});
})
.detach();
@ -5174,14 +5202,16 @@ impl Project {
.detach();
}
pub fn update_local_worktree_settings(
fn update_local_worktree_settings(
&mut self,
worktree: &ModelHandle<Worktree>,
changes: &UpdatedEntriesSet,
cx: &mut ModelContext<Self>,
) {
let project_id = self.remote_id();
let worktree_id = worktree.id();
let worktree = worktree.read(cx).as_local().unwrap();
let remote_worktree_id = worktree.id();
let mut settings_contents = Vec::new();
for (path, _, change) in changes.iter() {
@ -5195,10 +5225,7 @@ impl Project {
let removed = *change == PathChange::Removed;
let abs_path = worktree.absolutize(path);
settings_contents.push(async move {
anyhow::Ok((
settings_dir,
(!removed).then_some(fs.load(&abs_path).await?),
))
(settings_dir, (!removed).then_some(fs.load(&abs_path).await))
});
}
}
@ -5207,19 +5234,30 @@ impl Project {
return;
}
let client = self.client.clone();
cx.spawn_weak(move |_, mut cx| async move {
let settings_contents = futures::future::join_all(settings_contents).await;
let settings_contents: Vec<(Arc<Path>, _)> =
futures::future::join_all(settings_contents).await;
cx.update(|cx| {
cx.update_global::<SettingsStore, _, _>(|store, cx| {
for entry in settings_contents {
if let Some((directory, file_content)) = entry.log_err() {
store
.set_local_settings(
worktree_id,
directory,
file_content.as_ref().map(String::as_str),
cx,
)
for (directory, file_content) in settings_contents {
let file_content = file_content.and_then(|content| content.log_err());
store
.set_local_settings(
worktree_id,
directory.clone(),
file_content.as_ref().map(String::as_str),
cx,
)
.log_err();
if let Some(remote_id) = project_id {
client
.send(proto::UpdateWorktreeSettings {
project_id: remote_id,
worktree_id: remote_worktree_id.to_proto(),
path: directory.to_string_lossy().into_owned(),
content: file_content,
})
.log_err();
}
}
@ -5467,6 +5505,30 @@ impl Project {
})
}
async fn handle_update_worktree_settings(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
if let Some(worktree) = this.worktree_for_id(worktree_id, cx) {
cx.update_global::<SettingsStore, _, _>(|store, cx| {
store
.set_local_settings(
worktree.id(),
PathBuf::from(&envelope.payload.path).into(),
envelope.payload.content.as_ref().map(String::as_str),
cx,
)
.log_err();
});
}
Ok(())
})
}
async fn handle_create_project_entry(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::CreateProjectEntry>,
@ -6557,8 +6619,8 @@ impl Project {
}
self.metadata_changed(cx);
for (id, _) in old_worktrees_by_id {
cx.emit(Event::WorktreeRemoved(id));
for id in old_worktrees_by_id.keys() {
cx.emit(Event::WorktreeRemoved(*id));
}
Ok(())
@ -6928,6 +6990,13 @@ impl WorktreeHandle {
WorktreeHandle::Weak(handle) => handle.upgrade(cx),
}
}
pub fn handle_id(&self) -> usize {
match self {
WorktreeHandle::Strong(handle) => handle.id(),
WorktreeHandle::Weak(handle) => handle.id(),
}
}
}
impl OpenBuffer {

View File

@ -132,6 +132,8 @@ message Envelope {
OnTypeFormatting on_type_formatting = 111;
OnTypeFormattingResponse on_type_formatting_response = 112;
UpdateWorktreeSettings update_worktree_settings = 113;
}
}
@ -339,6 +341,13 @@ message UpdateWorktree {
string abs_path = 10;
}
message UpdateWorktreeSettings {
uint64 project_id = 1;
uint64 worktree_id = 2;
string path = 3;
optional string content = 4;
}
message CreateProjectEntry {
uint64 project_id = 1;
uint64 worktree_id = 2;

View File

@ -236,6 +236,7 @@ messages!(
(UpdateProject, Foreground),
(UpdateProjectCollaborator, Foreground),
(UpdateWorktree, Foreground),
(UpdateWorktreeSettings, Foreground),
(UpdateDiffBase, Foreground),
(GetPrivateUserInfo, Foreground),
(GetPrivateUserInfoResponse, Foreground),
@ -345,6 +346,7 @@ entity_messages!(
UpdateProject,
UpdateProjectCollaborator,
UpdateWorktree,
UpdateWorktreeSettings,
UpdateDiffBase
);

View File

@ -359,6 +359,21 @@ impl SettingsStore {
Ok(())
}
/// Add or remove a set of local settings via a JSON string.
pub fn clear_local_settings(&mut self, root_id: usize, cx: &AppContext) -> Result<()> {
eprintln!("clearing local settings {root_id}");
self.local_deserialized_settings
.retain(|k, _| k.0 != root_id);
self.recompute_values(Some((root_id, "".as_ref())), cx)?;
Ok(())
}
pub fn local_settings(&self, root_id: usize) -> impl '_ + Iterator<Item = (Arc<Path>, String)> {
self.local_deserialized_settings
.range((root_id, Path::new("").into())..(root_id + 1, Path::new("").into()))
.map(|((_, path), content)| (path.clone(), serde_json::to_string(content).unwrap()))
}
pub fn json_schema(
&self,
schema_params: &SettingsJsonSchemaParams,