Detailed changes
@@ -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,
@@ -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");
@@ -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::*;
@@ -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 {}
@@ -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,
@@ -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>,
@@ -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 {
@@ -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;
@@ -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
);
@@ -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,