diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index de17cdc210..b9ffbc64d0 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -572,8 +572,12 @@ impl workspace::ItemView for ProjectDiagnosticsEditor { true } - fn save(&mut self, cx: &mut ViewContext) -> Task> { - self.editor.save(cx) + fn save( + &mut self, + project: ModelHandle, + cx: &mut ViewContext, + ) -> Task> { + self.editor.save(project, cx) } fn can_save_as(&self, _: &AppContext) -> bool { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 28ddac536b..21c30c6f35 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -220,11 +220,15 @@ impl ItemView for Editor { !self.buffer().read(cx).is_singleton() || self.project_path(cx).is_some() } - fn save(&mut self, cx: &mut ViewContext) -> Task> { + fn save( + &mut self, + project: ModelHandle, + cx: &mut ViewContext, + ) -> Task> { let buffer = self.buffer().clone(); cx.spawn(|editor, mut cx| async move { buffer - .update(&mut cx, |buffer, cx| buffer.format(cx).log_err()) + .update(&mut cx, |buffer, cx| buffer.format(project, cx).log_err()) .await; editor.update(&mut cx, |editor, cx| { editor.request_autoscroll(Autoscroll::Fit, cx) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index cd6e3dc140..8e0cdbe5bf 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -10,6 +10,7 @@ use language::{ Buffer, BufferChunks, BufferSnapshot, Chunk, DiagnosticEntry, Event, File, Language, Outline, OutlineItem, Selection, ToOffset as _, ToPoint as _, ToPointUtf16 as _, TransactionId, }; +use project::Project; use std::{ cell::{Ref, RefCell}, cmp, fmt, io, @@ -930,16 +931,25 @@ impl MultiBuffer { cx.emit(event.clone()); } - pub fn format(&mut self, cx: &mut ModelContext) -> Task> { - let mut format_tasks = Vec::new(); - for BufferState { buffer, .. } in self.buffers.borrow().values() { - format_tasks.push(buffer.update(cx, |buffer, cx| buffer.format(cx))); - } - - cx.spawn(|_, _| async move { - for format in format_tasks { - format.await?; - } + pub fn format( + &mut self, + project: ModelHandle, + cx: &mut ModelContext, + ) -> Task> { + let buffers = self + .buffers + .borrow() + .values() + .map(|state| state.buffer.clone()) + .collect(); + let transaction = project.update(cx, |project, cx| project.format(buffers, true, cx)); + cx.spawn(|this, mut cx| async move { + let transaction = transaction.await?; + this.update(&mut cx, |this, _| { + if !this.singleton { + this.push_transaction(&transaction.0); + } + }); Ok(()) }) } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 7664529272..63b0e912f8 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -201,9 +201,6 @@ pub trait File { cx: &mut MutableAppContext, ) -> Task>; - fn format_remote(&self, buffer_id: u64, cx: &mut MutableAppContext) - -> Option>>; - fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext); fn buffer_removed(&self, buffer_id: u64, cx: &mut MutableAppContext); @@ -278,10 +275,6 @@ impl File for FakeFile { cx.spawn(|_| async move { Ok((Default::default(), SystemTime::UNIX_EPOCH)) }) } - fn format_remote(&self, _: u64, _: &mut MutableAppContext) -> Option>> { - None - } - fn buffer_updated(&self, _: u64, _: Operation, _: &mut MutableAppContext) {} fn buffer_removed(&self, _: u64, _: &mut MutableAppContext) {} @@ -540,52 +533,6 @@ impl Buffer { self.file.as_deref() } - pub fn format(&mut self, cx: &mut ModelContext) -> Task> { - let file = if let Some(file) = self.file.as_ref() { - file - } else { - return Task::ready(Err(anyhow!("buffer has no file"))); - }; - - if let Some(LanguageServerState { server, .. }) = self.language_server.as_ref() { - let server = server.clone(); - let abs_path = file.as_local().unwrap().abs_path(cx); - let version = self.version(); - cx.spawn(|this, mut cx| async move { - let edits = server - .request::(lsp::DocumentFormattingParams { - text_document: lsp::TextDocumentIdentifier::new( - lsp::Url::from_file_path(&abs_path).unwrap(), - ), - options: Default::default(), - work_done_progress_params: Default::default(), - }) - .await?; - - if let Some(edits) = edits { - this.update(&mut cx, |this, cx| { - if this.version == version { - this.apply_lsp_edits(edits, None, cx)?; - Ok(()) - } else { - Err(anyhow!("buffer edited since starting to format")) - } - }) - } else { - Ok(()) - } - }) - } else { - let format = file.format_remote(self.remote_id(), cx.as_mut()); - cx.spawn(|_, _| async move { - if let Some(format) = format { - format.await?; - } - Ok(()) - }) - } - } - pub fn save( &mut self, cx: &mut ModelContext, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 85e5303dfd..356a515c1a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -348,7 +348,7 @@ impl Project { client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer), client.subscribe_to_entity(remote_id, cx, Self::handle_save_buffer), client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved), - client.subscribe_to_entity(remote_id, cx, Self::handle_format_buffer), + client.subscribe_to_entity(remote_id, cx, Self::handle_format_buffers), client.subscribe_to_entity(remote_id, cx, Self::handle_get_completions), client.subscribe_to_entity( remote_id, @@ -613,9 +613,7 @@ impl Project { }) .await?; let buffer = response.buffer.ok_or_else(|| anyhow!("missing buffer"))?; - this.update(&mut cx, |this, cx| { - this.deserialize_remote_buffer(buffer, cx) - }) + this.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) }) } @@ -1045,6 +1043,112 @@ impl Project { Ok(()) } + pub fn format( + &self, + buffers: HashSet>, + push_to_history: bool, + cx: &mut ModelContext, + ) -> Task> { + let mut local_buffers = Vec::new(); + let mut remote_buffers = None; + for buffer_handle in buffers { + let buffer = buffer_handle.read(cx); + let worktree; + if let Some(file) = File::from_dyn(buffer.file()) { + worktree = file.worktree.clone(); + if let Some(buffer_abs_path) = file.as_local().map(|f| f.abs_path(cx)) { + let lang_server; + if let Some(lang) = buffer.language() { + if let Some(server) = self + .language_servers + .get(&(worktree.read(cx).id(), lang.name().to_string())) + { + lang_server = server.clone(); + } else { + return Task::ready(Err(anyhow!( + "buffer {} does not have a language server", + buffer.remote_id() + ))); + }; + } else { + return Task::ready(Err(anyhow!("buffer does not have a language"))); + } + + local_buffers.push((buffer_handle, buffer_abs_path, lang_server)); + } else { + remote_buffers.get_or_insert(Vec::new()).push(buffer_handle); + } + } else { + return Task::ready(Err(anyhow!( + "buffer {} does not belong to any worktree", + buffer.remote_id() + ))); + } + } + + let remote_buffers = self.remote_id().zip(remote_buffers); + let client = self.client.clone(); + + cx.spawn(|this, mut cx| async move { + let mut project_transaction = ProjectTransaction::default(); + + if let Some((project_id, remote_buffers)) = remote_buffers { + let response = client + .request(proto::FormatBuffers { + project_id, + buffer_ids: remote_buffers + .iter() + .map(|buffer| buffer.read_with(&cx, |buffer, _| buffer.remote_id())) + .collect(), + }) + .await? + .transaction + .ok_or_else(|| anyhow!("missing transaction"))?; + project_transaction = this + .update(&mut cx, |this, cx| { + this.deserialize_project_transaction(response, push_to_history, cx) + }) + .await?; + } + + for (buffer, buffer_abs_path, lang_server) in local_buffers { + let lsp_edits = lang_server + .request::(lsp::DocumentFormattingParams { + text_document: lsp::TextDocumentIdentifier::new( + lsp::Url::from_file_path(&buffer_abs_path).unwrap(), + ), + options: Default::default(), + work_done_progress_params: Default::default(), + }) + .await?; + + if let Some(lsp_edits) = lsp_edits { + let edits = buffer + .update(&mut cx, |buffer, cx| { + buffer.edits_from_lsp(lsp_edits, None, cx) + }) + .await?; + buffer.update(&mut cx, |buffer, cx| { + buffer.finalize_last_transaction(); + buffer.start_transaction(); + for (range, text) in edits { + buffer.edit([range], text, cx); + } + if buffer.end_transaction(cx).is_some() { + let transaction = buffer.finalize_last_transaction().unwrap().clone(); + if !push_to_history { + buffer.forget_transaction(transaction.id); + } + project_transaction.0.insert(cx.handle(), transaction); + } + }); + } + } + + Ok(project_transaction) + }) + } + pub fn definition( &self, source_buffer_handle: &ModelHandle, @@ -1156,7 +1260,7 @@ impl Project { this.update(&mut cx, |this, cx| { let mut definitions = Vec::new(); for definition in response.definitions { - let target_buffer = this.deserialize_remote_buffer( + let target_buffer = this.deserialize_buffer( definition.buffer.ok_or_else(|| anyhow!("missing buffer"))?, cx, )?; @@ -1637,29 +1741,10 @@ impl Project { .await? .transaction .ok_or_else(|| anyhow!("missing transaction"))?; - let mut project_transaction = ProjectTransaction::default(); - for (buffer, transaction) in response.buffers.into_iter().zip(response.transactions) - { - let buffer = this.update(&mut cx, |this, cx| { - this.deserialize_remote_buffer(buffer, cx) - })?; - let transaction = language::proto::deserialize_transaction(transaction)?; - - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_edits(transaction.edit_ids.iter().copied()) - }) - .await; - - if push_to_history { - buffer.update(&mut cx, |buffer, _| { - buffer.push_transaction(transaction.clone(), Instant::now()); - }); - } - - project_transaction.0.insert(buffer, transaction); - } - Ok(project_transaction) + this.update(&mut cx, |this, cx| { + this.deserialize_project_transaction(response, push_to_history, cx) + }) + .await }) } else { Task::ready(Err(anyhow!("project does not have a remote id"))) @@ -2163,26 +2248,51 @@ impl Project { Ok(()) } - pub fn handle_format_buffer( + pub fn handle_format_buffers( &mut self, - envelope: TypedEnvelope, + envelope: TypedEnvelope, rpc: Arc, cx: &mut ModelContext, ) -> Result<()> { let receipt = envelope.receipt(); let sender_id = envelope.original_sender_id()?; - let buffer = self + let shared_buffers = self .shared_buffers .get(&sender_id) - .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned()) - .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?; - cx.spawn(|_, mut cx| async move { - let format = buffer.update(&mut cx, |buffer, cx| buffer.format(cx)).await; - // We spawn here in order to enqueue the sending of `Ack` *after* transmission of edits - // associated with formatting. + .ok_or_else(|| anyhow!("peer has no buffers"))?; + let mut buffers = HashSet::default(); + for buffer_id in envelope.payload.buffer_ids { + buffers.insert( + shared_buffers + .get(&buffer_id) + .cloned() + .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?, + ); + } + cx.spawn(|this, mut cx| async move { + dbg!("here!"); + let project_transaction = this + .update(&mut cx, |this, cx| this.format(buffers, false, cx)) + .await + .map(|project_transaction| { + this.update(&mut cx, |this, cx| { + this.serialize_project_transaction_for_peer( + project_transaction, + sender_id, + cx, + ) + }) + }); + // We spawn here in order to enqueue the sending of the response *after* transmission of + // edits associated with formatting. cx.spawn(|_| async move { - match format { - Ok(()) => rpc.respond(receipt, proto::Ack {})?, + match project_transaction { + Ok(transaction) => rpc.respond( + receipt, + proto::FormatBuffersResponse { + transaction: Some(transaction), + }, + )?, Err(error) => rpc.respond_with_error( receipt, proto::Error { @@ -2358,18 +2468,11 @@ impl Project { cx.spawn(|this, mut cx| async move { match apply_code_action.await { Ok(project_transaction) => this.update(&mut cx, |this, cx| { - let mut serialized_transaction = proto::ProjectTransaction { - buffers: Default::default(), - transactions: Default::default(), - }; - for (buffer, transaction) in project_transaction.0 { - serialized_transaction - .buffers - .push(this.serialize_buffer_for_peer(&buffer, sender_id, cx)); - serialized_transaction - .transactions - .push(language::proto::serialize_transaction(&transaction)); - } + let serialized_transaction = this.serialize_project_transaction_for_peer( + project_transaction, + sender_id, + cx, + ); rpc.respond( receipt, proto::ApplyCodeActionResponse { @@ -2471,6 +2574,58 @@ impl Project { Ok(()) } + fn serialize_project_transaction_for_peer( + &mut self, + project_transaction: ProjectTransaction, + peer_id: PeerId, + cx: &AppContext, + ) -> proto::ProjectTransaction { + let mut serialized_transaction = proto::ProjectTransaction { + buffers: Default::default(), + transactions: Default::default(), + }; + for (buffer, transaction) in project_transaction.0 { + serialized_transaction + .buffers + .push(self.serialize_buffer_for_peer(&buffer, peer_id, cx)); + serialized_transaction + .transactions + .push(language::proto::serialize_transaction(&transaction)); + } + serialized_transaction + } + + fn deserialize_project_transaction( + &self, + message: proto::ProjectTransaction, + push_to_history: bool, + cx: &mut ModelContext, + ) -> Task> { + cx.spawn(|this, mut cx| async move { + let mut project_transaction = ProjectTransaction::default(); + for (buffer, transaction) in message.buffers.into_iter().zip(message.transactions) { + let buffer = + this.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))?; + let transaction = language::proto::deserialize_transaction(transaction)?; + + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_edits(transaction.edit_ids.iter().copied()) + }) + .await; + + if push_to_history { + buffer.update(&mut cx, |buffer, _| { + buffer.push_transaction(transaction.clone(), Instant::now()); + }); + } + + project_transaction.0.insert(buffer, transaction); + } + Ok(project_transaction) + }) + } + fn serialize_buffer_for_peer( &mut self, buffer: &ModelHandle, @@ -2492,7 +2647,7 @@ impl Project { } } - fn deserialize_remote_buffer( + fn deserialize_buffer( &mut self, buffer: proto::Buffer, cx: &mut ModelContext, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index f15423a7b7..94cfdb3150 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1385,25 +1385,6 @@ impl language::File for File { }) } - fn format_remote( - &self, - buffer_id: u64, - cx: &mut MutableAppContext, - ) -> Option>> { - let worktree = self.worktree.read(cx); - let worktree = worktree.as_remote()?; - let rpc = worktree.client.clone(); - let project_id = worktree.project_id; - Some(cx.foreground().spawn(async move { - rpc.request(proto::FormatBuffer { - project_id, - buffer_id, - }) - .await?; - Ok(()) - })) - } - fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext) { self.worktree.update(cx, |worktree, cx| { worktree.send_buffer_update(buffer_id, operation, cx); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index a71cd37dcd..2a96b762e1 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -39,31 +39,32 @@ message Envelope { SaveBuffer save_buffer = 31; BufferSaved buffer_saved = 32; BufferReloaded buffer_reloaded = 33; - FormatBuffer format_buffer = 34; - GetCompletions get_completions = 35; - GetCompletionsResponse get_completions_response = 36; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 37; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 38; - GetCodeActions get_code_actions = 39; - GetCodeActionsResponse get_code_actions_response = 40; - ApplyCodeAction apply_code_action = 41; - ApplyCodeActionResponse apply_code_action_response = 42; + FormatBuffers format_buffers = 34; + FormatBuffersResponse format_buffers_response = 35; + GetCompletions get_completions = 36; + GetCompletionsResponse get_completions_response = 37; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 38; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 39; + GetCodeActions get_code_actions = 40; + GetCodeActionsResponse get_code_actions_response = 41; + ApplyCodeAction apply_code_action = 42; + ApplyCodeActionResponse apply_code_action_response = 43; - GetChannels get_channels = 43; - GetChannelsResponse get_channels_response = 44; - JoinChannel join_channel = 45; - JoinChannelResponse join_channel_response = 46; - LeaveChannel leave_channel = 47; - SendChannelMessage send_channel_message = 48; - SendChannelMessageResponse send_channel_message_response = 49; - ChannelMessageSent channel_message_sent = 50; - GetChannelMessages get_channel_messages = 51; - GetChannelMessagesResponse get_channel_messages_response = 52; + GetChannels get_channels = 44; + GetChannelsResponse get_channels_response = 45; + JoinChannel join_channel = 46; + JoinChannelResponse join_channel_response = 47; + LeaveChannel leave_channel = 48; + SendChannelMessage send_channel_message = 49; + SendChannelMessageResponse send_channel_message_response = 50; + ChannelMessageSent channel_message_sent = 51; + GetChannelMessages get_channel_messages = 52; + GetChannelMessagesResponse get_channel_messages_response = 53; - UpdateContacts update_contacts = 53; + UpdateContacts update_contacts = 54; - GetUsers get_users = 54; - GetUsersResponse get_users_response = 55; + GetUsers get_users = 55; + GetUsersResponse get_users_response = 56; } } @@ -206,9 +207,13 @@ message BufferReloaded { Timestamp mtime = 4; } -message FormatBuffer { +message FormatBuffers { uint64 project_id = 1; - uint64 buffer_id = 2; + repeated uint64 buffer_ids = 2; +} + +message FormatBuffersResponse { + ProjectTransaction transaction = 1; } message GetCompletions { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index a22027173f..9aa9eb61b3 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -133,7 +133,8 @@ messages!( DiskBasedDiagnosticsUpdated, DiskBasedDiagnosticsUpdating, Error, - FormatBuffer, + FormatBuffers, + FormatBuffersResponse, GetChannelMessages, GetChannelMessagesResponse, GetChannels, @@ -180,7 +181,7 @@ request_messages!( ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEditsResponse ), - (FormatBuffer, Ack), + (FormatBuffers, FormatBuffersResponse), (GetChannelMessages, GetChannelMessagesResponse), (GetChannels, GetChannelsResponse), (GetCodeActions, GetCodeActionsResponse), @@ -210,7 +211,7 @@ entity_messages!( CloseBuffer, DiskBasedDiagnosticsUpdated, DiskBasedDiagnosticsUpdating, - FormatBuffer, + FormatBuffers, GetCodeActions, GetCompletions, GetDefinition, diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 677d1e979f..ce0270cc75 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -82,7 +82,7 @@ impl Server { .add_handler(Server::buffer_reloaded) .add_handler(Server::buffer_saved) .add_handler(Server::save_buffer) - .add_handler(Server::format_buffer) + .add_handler(Server::format_buffers) .add_handler(Server::get_completions) .add_handler(Server::apply_additional_edits_for_completion) .add_handler(Server::get_code_actions) @@ -669,9 +669,9 @@ impl Server { Ok(()) } - async fn format_buffer( + async fn format_buffers( self: Arc, - request: TypedEnvelope, + request: TypedEnvelope, ) -> tide::Result<()> { let host; { @@ -2607,7 +2607,9 @@ mod tests { .await .unwrap(); - let format = buffer_b.update(&mut cx_b, |buffer, cx| buffer.format(cx)); + let format = project_b.update(&mut cx_b, |project, cx| { + project.format(HashSet::from_iter([buffer_b.clone()]), true, cx) + }); let (request_id, _) = fake_language_server .receive_request::() .await; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c52e55141d..3cf63ade2e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -168,7 +168,11 @@ pub trait ItemView: View { false } fn can_save(&self, cx: &AppContext) -> bool; - fn save(&mut self, cx: &mut ViewContext) -> Task>; + fn save( + &mut self, + project: ModelHandle, + cx: &mut ViewContext, + ) -> Task>; fn can_save_as(&self, cx: &AppContext) -> bool; fn save_as( &mut self, @@ -234,7 +238,7 @@ pub trait ItemViewHandle: 'static { fn has_conflict(&self, cx: &AppContext) -> bool; fn can_save(&self, cx: &AppContext) -> bool; fn can_save_as(&self, cx: &AppContext) -> bool; - fn save(&self, cx: &mut MutableAppContext) -> Task>; + fn save(&self, project: ModelHandle, cx: &mut MutableAppContext) -> Task>; fn save_as( &self, project: ModelHandle, @@ -402,8 +406,8 @@ impl ItemViewHandle for ViewHandle { self.update(cx, |this, cx| this.navigate(data, cx)); } - fn save(&self, cx: &mut MutableAppContext) -> Task> { - self.update(cx, |item, cx| item.save(cx)) + fn save(&self, project: ModelHandle, cx: &mut MutableAppContext) -> Task> { + self.update(cx, |item, cx| item.save(project, cx)) } fn save_as( @@ -820,6 +824,7 @@ impl Workspace { } pub fn save_active_item(&mut self, cx: &mut ViewContext) -> Task> { + let project = self.project.clone(); if let Some(item) = self.active_item(cx) { if item.can_save(cx) { if item.has_conflict(cx.as_ref()) { @@ -833,12 +838,12 @@ impl Workspace { cx.spawn(|_, mut cx| async move { let answer = answer.recv().await; if answer == Some(0) { - cx.update(|cx| item.save(cx)).await?; + cx.update(|cx| item.save(project, cx)).await?; } Ok(()) }) } else { - item.save(cx) + item.save(project, cx) } } else if item.can_save_as(cx) { let worktree = self.worktrees(cx).next(); @@ -847,9 +852,8 @@ impl Workspace { .map_or(Path::new(""), |w| w.abs_path()) .to_path_buf(); let mut abs_path = cx.prompt_for_new_path(&start_abs_path); - cx.spawn(|this, mut cx| async move { + cx.spawn(|_, mut cx| async move { if let Some(abs_path) = abs_path.recv().await.flatten() { - let project = this.read_with(&cx, |this, _| this.project().clone()); cx.update(|cx| item.save_as(project, abs_path, cx)).await?; } Ok(())