From da6ac6c55127bf0d2a7a88bf17cd4f0da8ee5f00 Mon Sep 17 00:00:00 2001 From: Cameron Mcloughlin Date: Tue, 14 Apr 2026 16:20:05 +0100 Subject: [PATCH] workspace: Add separate "format and save" action (#53710) Release Notes: - Added `workspace: format and save` action which always formats, regardless of settings --- 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(-) diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 223ed13142d91584eb1ec309e33e2442a3601782..4a6c8816a2c7b1b7eda112a2eed166861c5ecd79 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -1229,6 +1229,7 @@ async fn test_send_breakpoints_when_editor_has_been_saved( editor.save( SaveOptions { format: true, + force_format: false, autosave: false, }, project.clone(), diff --git a/crates/editor/src/document_colors.rs b/crates/editor/src/document_colors.rs index 8f8b70128ffc2bb66b2147baaa53d77e40c03c25..d62bb87404d454269012348f9ed7666ea8baee2b 100644 --- a/crates/editor/src/document_colors.rs +++ b/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(), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bca55b5d175572200a33517888fd5eecfad15c5b..2878c8f3b88e4c9beee65dcddabe0b3d9fa1de8f 100644 --- a/crates/editor/src/editor.rs +++ b/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, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index c29df272d35af5a69ba07c76cb7da3866786bd2b..b94e9f79b0ca2b155cb92b3fc43ba8795cee5244 100644 --- a/crates/editor/src/editor_tests.rs +++ b/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, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 55fbf1c8ff1bc470736af58165115c287fb6c9c8..9d062391a5c9ac0d8c0adcfa00a51269111d1e14 100644 --- a/crates/editor/src/items.rs +++ b/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, diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index a0708cae36cafd733c711df5bbab93af508510c1..c4aad77396f9731b9a4956b906230dd92b3af489 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -1973,6 +1973,7 @@ mod tests { buffer_editor.save( SaveOptions { format: false, + force_format: false, autosave: false, }, project.clone(), diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 7e7903674e3d883bfb98ac8d57b5f407237f66d1..441c1cf47d11953480f77e0b30f686d89d05eb11 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1298,6 +1298,7 @@ impl ProjectSearchView { this.save( SaveOptions { format: true, + force_format: false, autosave: false, }, project, diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 64647419e300357e360e3ac3f535d8bbcd076711..c4b664e24803e2e022d1bfb7e744a5b8193a703a 100644 --- a/crates/workspace/src/item.rs +++ b/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, } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 6e38fb7d63d98f32b396746948bd02a39b220338..f8906455a852dbf519f9ad9a11d722116a287510 100644 --- a/crates/workspace/src/pane.rs +++ b/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, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 7aa9b45c52d82df4dcef8d363d3f8e39deaf3ed7..b84ec0f176fb44616e88b6b8cca5170d9b65dc84 100644 --- a/crates/workspace/src/workspace.rs +++ b/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) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index bb4a8dc2950dd397fd71576c7da4d078bc1d43c7..848efaffbb8e0abbf8e5099b0b8bf7da6aa8f90f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4300,6 +4300,7 @@ mod tests { editor.save( SaveOptions { format: true, + force_format: false, autosave: false, }, project.clone(),