diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index fd67dad2e5e01347df6856f31e60a93d10a7175f..394dcd808ed74ea6424daaed55c41e90d0973c98 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -2244,7 +2244,7 @@ async fn test_propagate_saves_and_fs_changes( }); // Edit the buffer as the host and concurrently save as guest B. - let save_b = buffer_b.update(cx_b, |buf, cx| buf.save(cx)); + let save_b = cx_b.update(|cx| Project::save_buffer(buffer_b.clone(), cx)); buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx)); save_b.await.unwrap(); assert_eq!( @@ -2909,7 +2909,7 @@ async fn test_buffer_conflict_after_save( assert!(!buf.has_conflict()); }); - buffer_b.update(cx_b, |buf, cx| buf.save(cx)).await.unwrap(); + cx_b.update(|cx| Project::save_buffer(buffer_b.clone(), cx)).await.unwrap(); cx_a.foreground().forbid_parking(); buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty())); buffer_b.read_with(cx_b, |buf, _| { diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index 4783957dbba129479f94dbb2a1bf24ecd291da22..434006c5c145bfee78bb93623d45540b7b161363 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -1064,15 +1064,16 @@ async fn randomly_query_and_mutate_buffers( } } 30..=39 if buffer.read_with(cx, |buffer, _| buffer.is_dirty()) => { - let (requested_version, save) = buffer.update(cx, |buffer, cx| { + let requested_version = buffer.update(cx, |buffer, cx| { log::info!( "{}: saving buffer {} ({:?})", client.username, buffer.remote_id(), buffer.file().unwrap().full_path(cx) ); - (buffer.version(), buffer.save(cx)) + buffer.version() }); + let save = cx.update(|cx| Project::save_buffer(buffer, cx)); let save = cx.background().spawn(async move { let (saved_version, _, _) = save .await diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 2c8503b8c75a535eb114ac99e6794db664622a20..aac8701a2295669be2452b09294148401d360ad1 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -608,20 +608,11 @@ impl Item for Editor { cx: &mut ViewContext, ) -> Task> { self.report_event("save editor", cx); - - let buffer = self.buffer().clone(); - let buffers = buffer.read(cx).all_buffers(); - let save = project.update(cx, |project, cx| project.save_buffers(buffers, cx)); + let format = self.perform_format(project.clone(), cx); + let buffers = self.buffer().clone().read(cx).all_buffers(); cx.spawn(|_, mut cx| async move { - let (format_transaction, save) = save.await; - buffer.update(&mut cx, |buffer, _| { - if let Some(transaction) = format_transaction { - if !buffer.is_singleton() { - buffer.push_transaction(&transaction.0); - } - } - }); - save.await?; + format.await?; + cx.update(|cx| Project::save_buffers(buffers, cx)).await?; Ok(()) }) } @@ -1144,7 +1135,6 @@ fn path_for_file<'a>( mod tests { use super::*; use gpui::MutableAppContext; - use language::RopeFingerprint; use std::{ path::{Path, PathBuf}, sync::Arc, @@ -1190,17 +1180,6 @@ mod tests { todo!() } - fn save( - &self, - _: u64, - _: language::Rope, - _: clock::Global, - _: project::LineEnding, - _: &mut MutableAppContext, - ) -> gpui::Task> { - todo!() - } - fn as_any(&self) -> &dyn std::any::Any { todo!() } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index c59ec8915524f438bb8df4c0ca507279ef3d935d..ad9345a6bdbe60ef9b74f2cc4ed94f176ac94d36 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -214,15 +214,6 @@ pub trait File: Send + Sync { fn is_deleted(&self) -> bool; - fn save( - &self, - buffer_id: u64, - text: Rope, - version: clock::Global, - line_ending: LineEnding, - cx: &mut MutableAppContext, - ) -> Task>; - fn as_any(&self) -> &dyn Any; fn to_proto(&self) -> rpc::proto::File; @@ -529,33 +520,6 @@ impl Buffer { self.file.as_ref() } - pub fn save( - &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"))); - }; - let text = self.as_rope().clone(); - let version = self.version(); - let save = file.save( - self.remote_id(), - text, - version, - self.line_ending(), - cx.as_mut(), - ); - cx.spawn(|this, mut cx| async move { - let (version, fingerprint, mtime) = save.await?; - this.update(&mut cx, |this, cx| { - this.did_save(version.clone(), fingerprint, mtime, None, cx); - }); - Ok((version, fingerprint, mtime)) - }) - } - pub fn saved_version(&self) -> &clock::Global { &self.saved_version } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 7eb2b61990969baf873cac7268d857dacf2d0bb5..3cde06e317804ae429b87113bdff2d07500c331a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -28,8 +28,8 @@ use language::{ range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, OffsetRangeExt, - Operation, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, - Unclipped, + Operation, Patch, PointUtf16, RopeFingerprint, TextBufferSnapshot, ToOffset, ToPointUtf16, + Transaction, Unclipped, }; use lsp::{ DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer, LanguageString, @@ -59,7 +59,7 @@ use std::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, }, - time::{Duration, Instant}, + time::{Duration, Instant, SystemTime}, }; use terminal::{Terminal, TerminalBuilder}; use util::{debug_panic, defer, post_inc, ResultExt, TryFutureExt as _}; @@ -1429,33 +1429,30 @@ impl Project { } pub fn save_buffers( - &mut self, buffers: HashSet>, - cx: &mut ModelContext, - ) -> Task<(Option, Task>)> { - const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); - - let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse(); - let format = self.format(buffers.clone(), true, FormatTrigger::Save, cx); - cx.spawn(|_, cx| async move { - let transaction = futures::select_biased! { - _ = timeout => { - log::warn!("timed out waiting for formatting"); - None - } - transaction = format.log_err().fuse() => transaction, - }; + cx: &mut MutableAppContext, + ) -> Task> { + cx.spawn(|mut cx| async move { + let save_tasks = buffers + .into_iter() + .map(|buffer| cx.update(|cx| Self::save_buffer(buffer, cx))); + try_join_all(save_tasks).await?; + Ok(()) + }) + } - ( - transaction, - cx.spawn(|mut cx| async move { - let save_tasks = buffers - .iter() - .map(|buffer| buffer.update(&mut cx, |buffer, cx| buffer.save(cx))); - try_join_all(save_tasks).await?; - Ok(()) - }), - ) + pub fn save_buffer( + buffer: ModelHandle, + cx: &mut MutableAppContext, + ) -> Task> { + let Some(file) = File::from_dyn(buffer.read(cx).file()) else { + return Task::ready(Err(anyhow!("buffer doesn't have a file"))); + }; + let worktree = file.worktree.clone(); + let path = file.path.clone(); + worktree.update(cx, |worktree, cx| match worktree { + Worktree::Local(worktree) => worktree.save_buffer(buffer, path, cx), + Worktree::Remote(worktree) => worktree.save_buffer(buffer, cx), }) } @@ -1476,11 +1473,9 @@ impl Project { } let (worktree, path) = worktree_task.await?; worktree - .update(&mut cx, |worktree, cx| { - worktree - .as_local_mut() - .unwrap() - .save_buffer_as(buffer.clone(), path, cx) + .update(&mut cx, |worktree, cx| match worktree { + Worktree::Local(worktree) => worktree.save_buffer_as(buffer.clone(), path, cx), + Worktree::Remote(_) => panic!("cannot remote buffers as new files"), }) .await?; this.update(&mut cx, |this, cx| { @@ -5187,7 +5182,7 @@ impl Project { .await; let (saved_version, fingerprint, mtime) = - buffer.update(&mut cx, |buffer, cx| buffer.save(cx)).await?; + cx.update(|cx| Self::save_buffer(buffer, cx)).await?; Ok(proto::BufferSaved { project_id, buffer_id, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 0ffa2553cde27ab9dc29cff80aa799e4eb884410..2a88714b18e1321423cf0e6964f96beb37d16517 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -243,8 +243,7 @@ async fn test_managing_language_servers( ); // Save notifications are reported to all servers. - toml_buffer - .update(cx, |buffer, cx| buffer.save(cx)) + cx.update(|cx| Project::save_buffer(toml_buffer, cx)) .await .unwrap(); assert_eq!( @@ -2083,12 +2082,12 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) { .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) .await .unwrap(); - buffer - .update(cx, |buffer, cx| { - assert_eq!(buffer.text(), "the old contents"); - buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); - buffer.save(cx) - }) + buffer.update(cx, |buffer, cx| { + assert_eq!(buffer.text(), "the old contents"); + buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); + }); + + cx.update(|cx| Project::save_buffer(buffer.clone(), cx)) .await .unwrap(); @@ -2112,11 +2111,11 @@ async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) .await .unwrap(); - buffer - .update(cx, |buffer, cx| { - buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); - buffer.save(cx) - }) + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); + }); + + cx.update(|cx| Project::save_buffer(buffer.clone(), cx)) .await .unwrap(); @@ -2703,11 +2702,10 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { }); // Save a file with windows line endings. The file is written correctly. - buffer2 - .update(cx, |buffer, cx| { - buffer.set_text("one\ntwo\nthree\nfour\n", cx); - buffer.save(cx) - }) + buffer2.update(cx, |buffer, cx| { + buffer.set_text("one\ntwo\nthree\nfour\n", cx); + }); + cx.update(|cx| Project::save_buffer(buffer2, cx)) .await .unwrap(); assert_eq!( diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index d743ba203179e54ada445e62acaf9880f05f3153..5edb22461849edc62e216b61f8cb4bf73e1f41e0 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -724,18 +724,55 @@ impl LocalWorktree { }) } + pub fn save_buffer( + &self, + buffer_handle: ModelHandle, + path: Arc, + cx: &mut ModelContext, + ) -> Task> { + let buffer = buffer_handle.read(cx); + + let rpc = self.client.clone(); + let buffer_id = buffer.remote_id(); + let project_id = self.share.as_ref().map(|share| share.project_id); + + let text = buffer.as_rope().clone(); + let fingerprint = text.fingerprint(); + let version = buffer.version(); + let save = self.write_file(path, text, buffer.line_ending(), cx); + + cx.as_mut().spawn(|mut cx| async move { + let mtime = save.await?.mtime; + if let Some(project_id) = project_id { + rpc.send(proto::BufferSaved { + project_id, + buffer_id, + version: serialize_version(&version), + mtime: Some(mtime.into()), + fingerprint: serialize_fingerprint(fingerprint), + })?; + } + buffer_handle.update(&mut cx, |buffer, cx| { + buffer.did_save(version.clone(), fingerprint, mtime, None, cx); + }); + anyhow::Ok((version, fingerprint, mtime)) + }) + } + pub fn save_buffer_as( &self, buffer_handle: ModelHandle, path: impl Into>, cx: &mut ModelContext, ) -> Task> { + let handle = cx.handle(); let buffer = buffer_handle.read(cx); + let text = buffer.as_rope().clone(); let fingerprint = text.fingerprint(); let version = buffer.version(); let save = self.write_file(path, text, buffer.line_ending(), cx); - let handle = cx.handle(); + cx.as_mut().spawn(|mut cx| async move { let entry = save.await?; let file = File { @@ -1085,6 +1122,39 @@ impl RemoteWorktree { self.disconnected = true; } + pub fn save_buffer( + &self, + buffer_handle: ModelHandle, + cx: &mut ModelContext, + ) -> Task> { + let buffer = buffer_handle.read(cx); + let buffer_id = buffer.remote_id(); + let version = buffer.version(); + let rpc = self.client.clone(); + let project_id = self.project_id; + cx.as_mut().spawn(|mut cx| async move { + let response = rpc + .request(proto::SaveBuffer { + project_id, + buffer_id, + version: serialize_version(&version), + }) + .await?; + let version = deserialize_version(response.version); + let fingerprint = deserialize_fingerprint(&response.fingerprint)?; + let mtime = response + .mtime + .ok_or_else(|| anyhow!("missing mtime"))? + .into(); + + buffer_handle.update(&mut cx, |buffer, cx| { + buffer.did_save(version.clone(), fingerprint, mtime, None, cx); + }); + + Ok((version, fingerprint, mtime)) + }) + } + pub fn update_from_remote(&mut self, update: proto::UpdateWorktree) { if let Some(updates_tx) = &self.updates_tx { updates_tx @@ -1859,57 +1929,6 @@ impl language::File for File { self.is_deleted } - fn save( - &self, - buffer_id: u64, - text: Rope, - version: clock::Global, - line_ending: LineEnding, - cx: &mut MutableAppContext, - ) -> Task> { - self.worktree.update(cx, |worktree, cx| match worktree { - Worktree::Local(worktree) => { - let rpc = worktree.client.clone(); - let project_id = worktree.share.as_ref().map(|share| share.project_id); - let fingerprint = text.fingerprint(); - let save = worktree.write_file(self.path.clone(), text, line_ending, cx); - cx.background().spawn(async move { - let entry = save.await?; - if let Some(project_id) = project_id { - rpc.send(proto::BufferSaved { - project_id, - buffer_id, - version: serialize_version(&version), - mtime: Some(entry.mtime.into()), - fingerprint: serialize_fingerprint(fingerprint), - })?; - } - Ok((version, fingerprint, entry.mtime)) - }) - } - Worktree::Remote(worktree) => { - let rpc = worktree.client.clone(); - let project_id = worktree.project_id; - cx.foreground().spawn(async move { - let response = rpc - .request(proto::SaveBuffer { - project_id, - buffer_id, - version: serialize_version(&version), - }) - .await?; - let version = deserialize_version(response.version); - let fingerprint = deserialize_fingerprint(&response.fingerprint)?; - let mtime = response - .mtime - .ok_or_else(|| anyhow!("missing mtime"))? - .into(); - Ok((version, fingerprint, mtime)) - }) - } - }) - } - fn as_any(&self) -> &dyn Any { self }