Move `Buffer::format` to `Project::format`

Antonio Scandurra created

Change summary

crates/diagnostics/src/diagnostics.rs |   8 
crates/editor/src/items.rs            |   8 
crates/editor/src/multi_buffer.rs     |  30 ++-
crates/language/src/buffer.rs         |  53 -----
crates/project/src/project.rs         | 259 +++++++++++++++++++++++-----
crates/project/src/worktree.rs        |  19 --
crates/rpc/proto/zed.proto            |  59 +++---
crates/rpc/src/proto.rs               |   7 
crates/server/src/rpc.rs              |  10 
crates/workspace/src/workspace.rs     |  20 +
10 files changed, 293 insertions(+), 180 deletions(-)

Detailed changes

crates/diagnostics/src/diagnostics.rs 🔗

@@ -572,8 +572,12 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
         true
     }
 
-    fn save(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
-        self.editor.save(cx)
+    fn save(
+        &mut self,
+        project: ModelHandle<Project>,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        self.editor.save(project, cx)
     }
 
     fn can_save_as(&self, _: &AppContext) -> bool {

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<Self>) -> Task<Result<()>> {
+    fn save(
+        &mut self,
+        project: ModelHandle<Project>,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
         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)

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<Self>) -> Task<Result<()>> {
-        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<Project>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        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(())
         })
     }

crates/language/src/buffer.rs 🔗

@@ -201,9 +201,6 @@ pub trait File {
         cx: &mut MutableAppContext,
     ) -> Task<Result<(clock::Global, SystemTime)>>;
 
-    fn format_remote(&self, buffer_id: u64, cx: &mut MutableAppContext)
-        -> Option<Task<Result<()>>>;
-
     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<Task<Result<()>>> {
-        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<Self>) -> Task<Result<()>> {
-        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::request::Formatting>(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<Self>,

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<ModelHandle<Buffer>>,
+        push_to_history: bool,
+        cx: &mut ModelContext<Project>,
+    ) -> Task<Result<ProjectTransaction>> {
+        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::request::Formatting>(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<T: ToPointUtf16>(
         &self,
         source_buffer_handle: &ModelHandle<Buffer>,
@@ -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<proto::FormatBuffer>,
+        envelope: TypedEnvelope<proto::FormatBuffers>,
         rpc: Arc<Client>,
         cx: &mut ModelContext<Self>,
     ) -> 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<Self>,
+    ) -> Task<Result<ProjectTransaction>> {
+        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<Buffer>,
@@ -2492,7 +2647,7 @@ impl Project {
         }
     }
 
-    fn deserialize_remote_buffer(
+    fn deserialize_buffer(
         &mut self,
         buffer: proto::Buffer,
         cx: &mut ModelContext<Self>,

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<Task<Result<()>>> {
-        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);

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;
-
-        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;
-
-        UpdateContacts update_contacts = 53;
-
-        GetUsers get_users = 54;
-        GetUsersResponse get_users_response = 55;
+        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 = 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 = 54;
+
+        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 {

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,

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<Server>,
-        request: TypedEnvelope<proto::FormatBuffer>,
+        request: TypedEnvelope<proto::FormatBuffers>,
     ) -> 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::<lsp::request::Formatting>()
             .await;

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<Self>) -> Task<Result<()>>;
+    fn save(
+        &mut self,
+        project: ModelHandle<Project>,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>>;
     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<Result<()>>;
+    fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>>;
     fn save_as(
         &self,
         project: ModelHandle<Project>,
@@ -402,8 +406,8 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
         self.update(cx, |this, cx| this.navigate(data, cx));
     }
 
-    fn save(&self, cx: &mut MutableAppContext) -> Task<Result<()>> {
-        self.update(cx, |item, cx| item.save(cx))
+    fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>> {
+        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<Self>) -> Task<Result<()>> {
+        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(())