diff --git a/Cargo.lock b/Cargo.lock index d74f09da1d..77dd2908ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,9 +63,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.42" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486" +checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" [[package]] name = "arrayref" diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 1813d8f52d..faf1da297d 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -126,6 +126,7 @@ impl Server { .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::update_buffer) .add_message_handler(Server::update_buffer_file) .add_message_handler(Server::buffer_reloaded) @@ -1808,6 +1809,73 @@ mod tests { .await; } + #[gpui::test(iterations = 10)] + async fn test_worktree_manipulation( + executor: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + ) { + executor.forbid_parking(); + let fs = FakeFs::new(cx_a.background()); + + // Connect to a server as 2 clients. + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let mut client_a = server.create_client(cx_a, "user_a").await; + let mut client_b = server.create_client(cx_b, "user_b").await; + + // Share a project as client A + fs.insert_tree( + "/dir", + json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, + "a.txt": "a-contents", + "b.txt": "b-contents", + }), + ) + .await; + + let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await; + let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap()); + project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + + let project_b = client_b.build_remote_project(project_id, cx_b).await; + + let worktree_a = + project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); + let worktree_b = + project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap()); + + project_b + .update(cx_b, |project, cx| { + project.create_file((worktree_id, "c.txt"), cx).unwrap() + }) + .await + .unwrap(); + + executor.run_until_parked(); + worktree_a.read_with(cx_a, |worktree, _| { + assert_eq!( + worktree + .paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + [".zed.toml", "a.txt", "b.txt", "c.txt"] + ); + }); + worktree_b.read_with(cx_b, |worktree, _| { + assert_eq!( + worktree + .paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + [".zed.toml", "a.txt", "b.txt", "c.txt"] + ); + }); + } + #[gpui::test(iterations = 10)] async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 2c80e01d6d..3126e18c7a 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -360,6 +360,14 @@ impl Deterministic { self.state.lock().now = new_now; } + + pub fn forbid_parking(&self) { + use rand::prelude::*; + + let mut state = self.state.lock(); + state.forbid_parking = true; + state.rng = StdRng::seed_from_u64(state.seed); + } } impl Drop for Timer { @@ -507,14 +515,8 @@ impl Foreground { #[cfg(any(test, feature = "test-support"))] pub fn forbid_parking(&self) { - use rand::prelude::*; - match self { - Self::Deterministic { executor, .. } => { - let mut state = executor.state.lock(); - state.forbid_parking = true; - state.rng = StdRng::seed_from_u64(state.seed); - } + Self::Deterministic { executor, .. } => executor.forbid_parking(), _ => panic!("this method can only be called on a deterministic executor"), } } diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 728dae3128..eaae45bcc6 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -29,7 +29,7 @@ settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } util = { path = "../util" } aho-corasick = "0.7" -anyhow = "1.0.38" +anyhow = "1.0.57" async-trait = "0.1" futures = "0.3" ignore = "0.4" diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 97697c26f2..307ef16d3a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -36,9 +36,11 @@ use std::{ cell::RefCell, cmp::{self, Ordering}, convert::TryInto, + ffi::OsString, hash::Hash, mem, ops::Range, + os::unix::{ffi::OsStrExt, prelude::OsStringExt}, path::{Component, Path, PathBuf}, rc::Rc, sync::{ @@ -259,6 +261,7 @@ impl Project { client.add_model_message_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_request_handler(Self::handle_create_project_entry); client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion); client.add_model_request_handler(Self::handle_apply_code_action); client.add_model_request_handler(Self::handle_reload_buffers); @@ -686,6 +689,47 @@ impl Project { .map(|worktree| worktree.read(cx).id()) } + pub fn create_file( + &mut self, + project_path: impl Into, + cx: &mut ModelContext, + ) -> Option>> { + let project_path = project_path.into(); + let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; + + if self.is_local() { + Some(worktree.update(cx, |worktree, cx| { + worktree.as_local_mut().unwrap().write_file( + project_path.path, + Default::default(), + cx, + ) + })) + } else { + let client = self.client.clone(); + let project_id = self.remote_id().unwrap(); + + Some(cx.spawn_weak(|_, mut cx| async move { + let response = client + .request(proto::CreateProjectEntry { + worktree_id: project_path.worktree_id.to_proto(), + project_id, + path: project_path.path.as_os_str().as_bytes().to_vec(), + is_directory: false, + }) + .await?; + worktree.update(&mut cx, |worktree, _| { + let worktree = worktree.as_remote_mut().unwrap(); + worktree.snapshot.insert_entry( + response + .entry + .ok_or_else(|| anyhow!("missing entry in response"))?, + ) + }) + })) + } + } + pub fn can_share(&self, cx: &AppContext) -> bool { self.is_local() && self.visible_worktrees(cx).next().is_some() } @@ -3733,6 +3777,34 @@ impl Project { }) } + async fn handle_create_project_entry( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let entry = this + .update(&mut cx, |this, cx| { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + let worktree = this + .worktree_for_id(worktree_id, cx) + .ok_or_else(|| anyhow!("worktree not found"))?; + worktree.update(cx, |worktree, cx| { + let worktree = worktree.as_local_mut().unwrap(); + if envelope.payload.is_directory { + unimplemented!("can't yet create directories"); + } else { + let path = PathBuf::from(OsString::from_vec(envelope.payload.path)); + anyhow::Ok(worktree.write_file(path, Default::default(), cx)) + } + }) + })? + .await?; + Ok(proto::CreateProjectEntryResponse { + entry: Some((&entry).into()), + }) + } + async fn handle_update_diagnostic_summary( this: ModelHandle, envelope: TypedEnvelope, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index a8e630c1a3..d4d6160a4d 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -42,6 +42,7 @@ use std::{ fmt, future::Future, ops::{Deref, DerefMut}, + os::unix::prelude::{OsStrExt, OsStringExt}, path::{Path, PathBuf}, sync::{atomic::AtomicUsize, Arc}, time::{Duration, SystemTime}, @@ -623,13 +624,15 @@ impl LocalWorktree { let handle = cx.handle(); let path = Arc::from(path); let abs_path = self.absolutize(&path); - let background_snapshot = self.background_snapshot.clone(); let fs = self.fs.clone(); cx.spawn(|this, mut cx| async move { let text = fs.load(&abs_path).await?; // Eagerly populate the snapshot with an updated entry for the loaded file - let entry = - refresh_entry(fs.as_ref(), &background_snapshot, path, &abs_path, None).await?; + let entry = this + .update(&mut cx, |this, _| { + this.as_local().unwrap().refresh_entry(path, abs_path, None) + }) + .await?; this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); Ok(( File { @@ -653,7 +656,7 @@ impl LocalWorktree { let buffer = buffer_handle.read(cx); let text = buffer.as_rope().clone(); let version = buffer.version(); - let save = self.save(path, text, cx); + let save = self.write_file(path, text, cx); let handle = cx.handle(); cx.as_mut().spawn(|mut cx| async move { let entry = save.await?; @@ -673,7 +676,7 @@ impl LocalWorktree { }) } - pub fn save( + pub fn write_file( &self, path: impl Into>, text: Rope, @@ -681,22 +684,21 @@ impl LocalWorktree { ) -> Task> { let path = path.into(); let abs_path = self.absolutize(&path); - let background_snapshot = self.background_snapshot.clone(); - let fs = self.fs.clone(); - let save = cx.background().spawn(async move { - fs.save(&abs_path, &text).await?; - refresh_entry( - fs.as_ref(), - &background_snapshot, - path.clone(), - &abs_path, - None, - ) - .await + let save = cx.background().spawn({ + let fs = self.fs.clone(); + let abs_path = abs_path.clone(); + async move { fs.save(&abs_path, &text).await } }); cx.spawn(|this, mut cx| async move { - let entry = save.await?; + save.await?; + let entry = this + .update(&mut cx, |this, _| { + this.as_local_mut() + .unwrap() + .refresh_entry(path, abs_path, None) + }) + .await?; this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); Ok(entry) }) @@ -712,28 +714,68 @@ impl LocalWorktree { let new_path = new_path.into(); let abs_old_path = self.absolutize(&old_path); let abs_new_path = self.absolutize(&new_path); - let background_snapshot = self.background_snapshot.clone(); - let fs = self.fs.clone(); - let rename = cx.background().spawn(async move { - fs.rename(&abs_old_path, &abs_new_path, Default::default()) - .await?; - refresh_entry( - fs.as_ref(), - &background_snapshot, - new_path.clone(), - &abs_new_path, - Some(old_path), - ) - .await + let rename = cx.background().spawn({ + let fs = self.fs.clone(); + let abs_new_path = abs_new_path.clone(); + async move { + fs.rename(&abs_old_path, &abs_new_path, Default::default()) + .await + } }); cx.spawn(|this, mut cx| async move { - let entry = rename.await?; + rename.await?; + let entry = this + .update(&mut cx, |this, _| { + this.as_local_mut().unwrap().refresh_entry( + new_path.clone(), + abs_new_path, + Some(old_path), + ) + }) + .await?; this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); Ok(entry) }) } + fn refresh_entry( + &self, + path: Arc, + abs_path: PathBuf, + old_path: Option>, + ) -> impl Future> { + let root_char_bag; + let next_entry_id; + let fs = self.fs.clone(); + let shared_snapshots_tx = self.share.as_ref().map(|share| share.snapshots_tx.clone()); + let snapshot = self.background_snapshot.clone(); + { + let snapshot = snapshot.lock(); + root_char_bag = snapshot.root_char_bag; + next_entry_id = snapshot.next_entry_id.clone(); + } + async move { + let entry = Entry::new( + path, + &fs.metadata(&abs_path) + .await? + .ok_or_else(|| anyhow!("could not read saved file metadata"))?, + &next_entry_id, + root_char_bag, + ); + let mut snapshot = snapshot.lock(); + if let Some(old_path) = old_path { + snapshot.remove_path(&old_path); + } + let entry = snapshot.insert_entry(entry, fs.as_ref()); + if let Some(tx) = shared_snapshots_tx { + tx.send(snapshot.clone()).await.ok(); + } + Ok(entry) + } + } + pub fn register( &mut self, project_id: u64, @@ -914,6 +956,21 @@ impl Snapshot { self.entries_by_id.get(&entry_id, &()).is_some() } + pub(crate) fn insert_entry(&mut self, entry: proto::Entry) -> Result { + let entry = Entry::try_from((&self.root_char_bag, entry))?; + self.entries_by_id.insert_or_replace( + PathEntry { + id: entry.id, + path: entry.path.clone(), + is_ignored: entry.is_ignored, + scan_id: 0, + }, + &(), + ); + self.entries_by_path.insert_or_replace(entry.clone(), &()); + Ok(entry) + } + pub(crate) fn apply_remote_update(&mut self, update: proto::UpdateWorktree) -> Result<()> { let mut entries_by_path_edits = Vec::new(); let mut entries_by_id_edits = Vec::new(); @@ -1437,7 +1494,7 @@ impl language::File for File { Worktree::Local(worktree) => { let rpc = worktree.client.clone(); let project_id = worktree.share.as_ref().map(|share| share.project_id); - let save = worktree.save(self.path.clone(), text, cx); + let save = worktree.write_file(self.path.clone(), text, cx); cx.background().spawn(async move { let entry = save.await?; if let Some(project_id) = project_id { @@ -2155,35 +2212,6 @@ impl BackgroundScanner { } } -async fn refresh_entry( - fs: &dyn Fs, - snapshot: &Mutex, - path: Arc, - abs_path: &Path, - old_path: Option>, -) -> Result { - let root_char_bag; - let next_entry_id; - { - let snapshot = snapshot.lock(); - root_char_bag = snapshot.root_char_bag; - next_entry_id = snapshot.next_entry_id.clone(); - } - let entry = Entry::new( - path, - &fs.metadata(abs_path) - .await? - .ok_or_else(|| anyhow!("could not read saved file metadata"))?, - &next_entry_id, - root_char_bag, - ); - let mut snapshot = snapshot.lock(); - if let Some(old_path) = old_path { - snapshot.remove_path(&old_path); - } - Ok(snapshot.insert_entry(entry, fs)) -} - fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { let mut result = root_char_bag; result.extend( @@ -2421,7 +2449,7 @@ impl<'a> From<&'a Entry> for proto::Entry { Self { id: entry.id.to_proto(), is_dir: entry.is_dir(), - path: entry.path.to_string_lossy().to_string(), + path: entry.path.as_os_str().as_bytes().to_vec(), inode: entry.inode, mtime: Some(entry.mtime.into()), is_symlink: entry.is_symlink, @@ -2439,10 +2467,14 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { EntryKind::Dir } else { let mut char_bag = root_char_bag.clone(); - char_bag.extend(entry.path.chars().map(|c| c.to_ascii_lowercase())); + char_bag.extend( + String::from_utf8_lossy(&entry.path) + .chars() + .map(|c| c.to_ascii_lowercase()), + ); EntryKind::File(char_bag) }; - let path: Arc = Arc::from(Path::new(&entry.path)); + let path: Arc = PathBuf::from(OsString::from_vec(entry.path)).into(); Ok(Entry { id: ProjectEntryId::from_proto(entry.id), kind, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 90d0e30b65..0ee26d9c39 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -273,27 +273,19 @@ impl ProjectPanel { fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) -> Option>> { let edit_state = self.edit_state.take()?; cx.focus_self(); + let worktree = self .project .read(cx) .worktree_for_id(edit_state.worktree_id, cx)?; - - // TODO - implement this for remote projects - if !worktree.read(cx).is_local() { - return None; - } - let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?; let filename = self.filename_editor.read(cx).text(cx); if edit_state.new_file { let new_path = entry.path.join(filename); - let save = worktree.update(cx, |worktree, cx| { - worktree - .as_local() - .unwrap() - .save(new_path, Default::default(), cx) - }); + let save = self.project.update(cx, |project, cx| { + project.create_file((edit_state.worktree_id, new_path), cx) + })?; Some(cx.spawn(|this, mut cx| async move { let new_entry = save.await?; this.update(&mut cx, |this, cx| { @@ -303,6 +295,11 @@ impl ProjectPanel { Ok(()) })) } else { + // TODO - implement this for remote projects + if !worktree.read(cx).is_local() { + return None; + } + let old_path = entry.path.clone(); let new_path = if let Some(parent) = old_path.parent() { parent.join(filename) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index bf18db9e2b..3fc350e9b6 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -36,57 +36,63 @@ message Envelope { RegisterWorktree register_worktree = 28; UnregisterWorktree unregister_worktree = 29; UpdateWorktree update_worktree = 31; - UpdateDiagnosticSummary update_diagnostic_summary = 32; - StartLanguageServer start_language_server = 33; - UpdateLanguageServer update_language_server = 34; - OpenBufferById open_buffer_by_id = 35; - OpenBufferByPath open_buffer_by_path = 36; - OpenBufferResponse open_buffer_response = 37; - UpdateBuffer update_buffer = 38; - UpdateBufferFile update_buffer_file = 39; - SaveBuffer save_buffer = 40; - BufferSaved buffer_saved = 41; - BufferReloaded buffer_reloaded = 42; - ReloadBuffers reload_buffers = 43; - ReloadBuffersResponse reload_buffers_response = 44; - FormatBuffers format_buffers = 45; - FormatBuffersResponse format_buffers_response = 46; - GetCompletions get_completions = 47; - GetCompletionsResponse get_completions_response = 48; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 49; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 50; - GetCodeActions get_code_actions = 51; - GetCodeActionsResponse get_code_actions_response = 52; - ApplyCodeAction apply_code_action = 53; - ApplyCodeActionResponse apply_code_action_response = 54; - PrepareRename prepare_rename = 55; - PrepareRenameResponse prepare_rename_response = 56; - PerformRename perform_rename = 57; - PerformRenameResponse perform_rename_response = 58; - SearchProject search_project = 59; - SearchProjectResponse search_project_response = 60; + CreateProjectEntry create_project_entry = 32; + CreateProjectEntryResponse create_project_entry_response = 33; + RenameProjectEntry rename_project_entry = 34; + DeleteProjectEntry delete_project_entry = 35; - GetChannels get_channels = 61; - GetChannelsResponse get_channels_response = 62; - JoinChannel join_channel = 63; - JoinChannelResponse join_channel_response = 64; - LeaveChannel leave_channel = 65; - SendChannelMessage send_channel_message = 66; - SendChannelMessageResponse send_channel_message_response = 67; - ChannelMessageSent channel_message_sent = 68; - GetChannelMessages get_channel_messages = 69; - GetChannelMessagesResponse get_channel_messages_response = 70; + UpdateDiagnosticSummary update_diagnostic_summary = 36; + StartLanguageServer start_language_server = 37; + UpdateLanguageServer update_language_server = 38; - UpdateContacts update_contacts = 71; + OpenBufferById open_buffer_by_id = 39; + OpenBufferByPath open_buffer_by_path = 40; + OpenBufferResponse open_buffer_response = 41; + UpdateBuffer update_buffer = 42; + UpdateBufferFile update_buffer_file = 43; + SaveBuffer save_buffer = 44; + BufferSaved buffer_saved = 45; + BufferReloaded buffer_reloaded = 46; + ReloadBuffers reload_buffers = 47; + ReloadBuffersResponse reload_buffers_response = 48; + FormatBuffers format_buffers = 49; + FormatBuffersResponse format_buffers_response = 50; + GetCompletions get_completions = 51; + GetCompletionsResponse get_completions_response = 52; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 53; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 54; + GetCodeActions get_code_actions = 55; + GetCodeActionsResponse get_code_actions_response = 56; + ApplyCodeAction apply_code_action = 57; + ApplyCodeActionResponse apply_code_action_response = 58; + PrepareRename prepare_rename = 59; + PrepareRenameResponse prepare_rename_response = 60; + PerformRename perform_rename = 61; + PerformRenameResponse perform_rename_response = 62; + SearchProject search_project = 63; + SearchProjectResponse search_project_response = 64; - GetUsers get_users = 72; - GetUsersResponse get_users_response = 73; + GetChannels get_channels = 65; + GetChannelsResponse get_channels_response = 66; + JoinChannel join_channel = 67; + JoinChannelResponse join_channel_response = 68; + LeaveChannel leave_channel = 69; + SendChannelMessage send_channel_message = 70; + SendChannelMessageResponse send_channel_message_response = 71; + ChannelMessageSent channel_message_sent = 72; + GetChannelMessages get_channel_messages = 73; + GetChannelMessagesResponse get_channel_messages_response = 74; - Follow follow = 74; - FollowResponse follow_response = 75; - UpdateFollowers update_followers = 76; - Unfollow unfollow = 77; + UpdateContacts update_contacts = 75; + + GetUsers get_users = 76; + GetUsersResponse get_users_response = 77; + + Follow follow = 78; + FollowResponse follow_response = 79; + UpdateFollowers update_followers = 80; + Unfollow unfollow = 81; } } @@ -158,6 +164,31 @@ message UpdateWorktree { repeated uint64 removed_entries = 5; } +message CreateProjectEntry { + uint64 project_id = 1; + uint64 worktree_id = 2; + bytes path = 3; + bool is_directory = 4; +} + +message CreateProjectEntryResponse { + Entry entry = 1; +} + +message RenameProjectEntry { + uint64 project_id = 1; + uint64 old_worktree_id = 2; + string old_path = 3; + uint64 new_worktree_id = 4; + string new_path = 5; +} + +message DeleteProjectEntry { + uint64 project_id = 1; + uint64 worktree_id = 2; + string path = 3; +} + message AddProjectCollaborator { uint64 project_id = 1; Collaborator collaborator = 2; @@ -642,7 +673,7 @@ message File { message Entry { uint64 id = 1; bool is_dir = 2; - string path = 3; + bytes path = 3; uint64 inode = 4; Timestamp mtime = 5; bool is_symlink = 6; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 98fc493774..a1c28075f5 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -147,6 +147,9 @@ messages!( (BufferReloaded, Foreground), (BufferSaved, Foreground), (ChannelMessageSent, Foreground), + (CreateProjectEntry, Foreground), + (CreateProjectEntryResponse, Foreground), + (DeleteProjectEntry, Foreground), (Error, Foreground), (Follow, Foreground), (FollowResponse, Foreground), @@ -194,6 +197,7 @@ messages!( (ReloadBuffers, Foreground), (ReloadBuffersResponse, Foreground), (RemoveProjectCollaborator, Foreground), + (RenameProjectEntry, Foreground), (SaveBuffer, Foreground), (SearchProject, Background), (SearchProjectResponse, Background), @@ -219,6 +223,7 @@ request_messages!( ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEditsResponse ), + (CreateProjectEntry, CreateProjectEntryResponse), (Follow, FollowResponse), (FormatBuffers, FormatBuffersResponse), (GetChannelMessages, GetChannelMessagesResponse), @@ -257,6 +262,9 @@ entity_messages!( ApplyCompletionAdditionalEdits, BufferReloaded, BufferSaved, + CreateProjectEntry, + RenameProjectEntry, + DeleteProjectEntry, Follow, FormatBuffers, GetCodeActions,