workspace: Add separate "format and save" action (#53710)

Cameron Mcloughlin created

Release Notes:

- Added `workspace: format and save` action which always formats,
regardless of settings

Change summary

crates/debugger_ui/src/tests/debugger_panel.rs |  1 +
crates/editor/src/document_colors.rs           |  2 ++
crates/editor/src/editor.rs                    |  2 ++
crates/editor/src/editor_tests.rs              | 13 +++++++++++++
crates/editor/src/items.rs                     |  8 +++++++-
crates/git_ui/src/project_diff.rs              |  1 +
crates/search/src/project_search.rs            |  1 +
crates/workspace/src/item.rs                   |  2 ++
crates/workspace/src/pane.rs                   | 12 +++++++++++-
crates/workspace/src/workspace.rs              |  7 +++++++
crates/zed/src/zed.rs                          |  1 +
11 files changed, 48 insertions(+), 2 deletions(-)

Detailed changes

crates/editor/src/document_colors.rs 🔗

@@ -602,6 +602,7 @@ mod tests {
             editor.save(
                 SaveOptions {
                     format: true,
+                    force_format: false,
                     autosave: true,
                 },
                 project.clone(),
@@ -692,6 +693,7 @@ mod tests {
             editor.save(
                 SaveOptions {
                     format: false,
+                    force_format: false,
                     autosave: true,
                 },
                 project.clone(),

crates/editor/src/editor.rs 🔗

@@ -21349,6 +21349,7 @@ impl Editor {
             self.save(
                 SaveOptions {
                     format: true,
+                    force_format: false,
                     autosave: false,
                 },
                 project,
@@ -21395,6 +21396,7 @@ impl Editor {
             self.save(
                 SaveOptions {
                     format: true,
+                    force_format: false,
                     autosave: false,
                 },
                 project,

crates/editor/src/editor_tests.rs 🔗

@@ -13471,6 +13471,7 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) {
                 editor.save(
                     SaveOptions {
                         format: true,
+                        force_format: false,
                         autosave: false,
                     },
                     project.clone(),
@@ -13510,6 +13511,7 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) {
                 editor.save(
                     SaveOptions {
                         format: true,
+                        force_format: false,
                         autosave: false,
                     },
                     project.clone(),
@@ -13556,6 +13558,7 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) {
                 editor.save(
                     SaveOptions {
                         format: true,
+                        force_format: false,
                         autosave: false,
                     },
                     project.clone(),
@@ -13642,6 +13645,7 @@ async fn test_auto_formatter_skips_server_without_formatting(cx: &mut TestAppCon
             editor.save(
                 SaveOptions {
                     format: true,
+                    force_format: false,
                     autosave: false,
                 },
                 project.clone(),
@@ -13712,6 +13716,7 @@ async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
                 editor.save(
                     SaveOptions {
                         format: true,
+                        force_format: false,
                         autosave: false,
                     },
                     project.clone(),
@@ -13931,6 +13936,7 @@ async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
             editor.save(
                 SaveOptions {
                     format: true,
+                    force_format: false,
                     autosave: false,
                 },
                 project.clone(),
@@ -14127,6 +14133,7 @@ async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
         editor.save(
             SaveOptions {
                 format: true,
+                force_format: false,
                 autosave: true,
             },
             project.clone(),
@@ -14163,6 +14170,7 @@ async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
         editor.save(
             SaveOptions {
                 format: true,
+                force_format: false,
                 autosave: false,
             },
             project.clone(),
@@ -14250,6 +14258,7 @@ async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
             editor.save(
                 SaveOptions {
                     format: true,
+                    force_format: false,
                     autosave: false,
                 },
                 project.clone(),
@@ -14305,6 +14314,7 @@ async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
             editor.save(
                 SaveOptions {
                     format: true,
+                    force_format: false,
                     autosave: false,
                 },
                 project.clone(),
@@ -14332,6 +14342,7 @@ async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext)
             editor.save(
                 SaveOptions {
                     format: false,
+                    force_format: false,
                     autosave: false,
                 },
                 project.clone(),
@@ -14373,6 +14384,7 @@ async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppC
             editor.save(
                 SaveOptions {
                     format: true,
+                    force_format: false,
                     autosave: false,
                 },
                 project.clone(),
@@ -31442,6 +31454,7 @@ async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
         editor.save(
             SaveOptions {
                 format: true,
+                force_format: false,
                 autosave: true,
             },
             project,

crates/editor/src/items.rs 🔗

@@ -889,12 +889,18 @@ impl Item for Editor {
                 .collect()
         };
 
+        let format_trigger = if options.force_format {
+            FormatTrigger::Manual
+        } else {
+            FormatTrigger::Save
+        };
+
         cx.spawn_in(window, async move |this, cx| {
             if options.format {
                 this.update_in(cx, |editor, window, cx| {
                     editor.perform_format(
                         project.clone(),
-                        FormatTrigger::Save,
+                        format_trigger,
                         FormatTarget::Buffers(buffers_to_save.clone()),
                         window,
                         cx,

crates/workspace/src/item.rs 🔗

@@ -39,6 +39,7 @@ pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200);
 #[derive(Clone, Copy, Debug)]
 pub struct SaveOptions {
     pub format: bool,
+    pub force_format: bool,
     pub autosave: bool,
 }
 
@@ -46,6 +47,7 @@ impl Default for SaveOptions {
     fn default() -> Self {
         Self {
             format: true,
+            force_format: false,
             autosave: false,
         }
     }

crates/workspace/src/pane.rs 🔗

@@ -83,6 +83,8 @@ pub enum SaveIntent {
     /// write all files (even if unchanged)
     /// prompt before overwriting on-disk changes
     Save,
+    /// same as Save, but always formats regardless of the format_on_save setting
+    FormatAndSave,
     /// same as Save, but without auto formatting
     SaveWithoutFormat,
     /// write any files that have local changes
@@ -2267,7 +2269,10 @@ impl Pane {
         })?;
 
         // when saving a single buffer, we ignore whether or not it's dirty.
-        if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
+        if save_intent == SaveIntent::Save
+            || save_intent == SaveIntent::FormatAndSave
+            || save_intent == SaveIntent::SaveWithoutFormat
+        {
             is_dirty = true;
         }
 
@@ -2282,6 +2287,7 @@ impl Pane {
         }
 
         let should_format = save_intent != SaveIntent::SaveWithoutFormat;
+        let force_format = save_intent == SaveIntent::FormatAndSave;
 
         if has_conflict && can_save {
             if has_deleted_file && is_singleton {
@@ -2301,6 +2307,7 @@ impl Pane {
                             item.save(
                                 SaveOptions {
                                     format: should_format,
+                                    force_format,
                                     autosave: false,
                                 },
                                 project,
@@ -2335,6 +2342,7 @@ impl Pane {
                             item.save(
                                 SaveOptions {
                                     format: should_format,
+                                    force_format,
                                     autosave: false,
                                 },
                                 project,
@@ -2416,6 +2424,7 @@ impl Pane {
                     item.save(
                         SaveOptions {
                             format: should_format,
+                            force_format,
                             autosave: false,
                         },
                         project,
@@ -2500,6 +2509,7 @@ impl Pane {
             item.save(
                 SaveOptions {
                     format,
+                    force_format: false,
                     autosave: true,
                 },
                 project,

crates/workspace/src/workspace.rs 🔗

@@ -303,6 +303,8 @@ actions!(
         ResetOpenDocksSize,
         /// Reloads the application
         Reload,
+        /// Formats and saves the current file, regardless of the format_on_save setting.
+        FormatAndSave,
         /// Saves the current file with a new name.
         SaveAs,
         /// Saves without formatting.
@@ -6942,6 +6944,11 @@ impl Workspace {
                     .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
                     .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
             }))
+            .on_action(cx.listener(|workspace, _: &FormatAndSave, window, cx| {
+                workspace
+                    .save_active_item(SaveIntent::FormatAndSave, window, cx)
+                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
+            }))
             .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
                 workspace
                     .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)

crates/zed/src/zed.rs 🔗

@@ -4300,6 +4300,7 @@ mod tests {
                     editor.save(
                         SaveOptions {
                             format: true,
+                            force_format: false,
                             autosave: false,
                         },
                         project.clone(),