diff --git a/assets/settings/default.json b/assets/settings/default.json index 1055d3a413..1f1a1795c3 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -219,9 +219,6 @@ "inline_code_actions": true, // Whether to allow drag and drop text selection in buffer. "drag_and_drop_selection": true, - // Whether to save singleton buffers that are not dirty. - // This will "touch" the file and related tools enabled, e.g. formatters. - "save_non_dirty_buffers": true, // What to do when go to definition yields no results. // // 1. Do nothing: `none` diff --git a/crates/agent/src/agent_diff.rs b/crates/agent/src/agent_diff.rs index 1516cd0228..e33327268c 100644 --- a/crates/agent/src/agent_diff.rs +++ b/crates/agent/src/agent_diff.rs @@ -31,7 +31,7 @@ use util::ResultExt; use workspace::{ Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, - item::{BreadcrumbText, ItemEvent, TabContentParams}, + item::{BreadcrumbText, ItemEvent, SaveOptions, TabContentParams}, searchable::SearchableItemHandle, }; use zed_actions::assistant::ToggleFocus; @@ -532,12 +532,12 @@ impl Item for AgentDiffPane { fn save( &mut self, - format: bool, + options: SaveOptions, project: Entity, window: &mut Window, cx: &mut Context, ) -> Task> { - self.editor.save(format, project, window, cx) + self.editor.save(options, project, window, cx) } fn save_as( diff --git a/crates/debugger_ui/src/stack_trace_view.rs b/crates/debugger_ui/src/stack_trace_view.rs index feb4bac8b2..f6087352dc 100644 --- a/crates/debugger_ui/src/stack_trace_view.rs +++ b/crates/debugger_ui/src/stack_trace_view.rs @@ -16,7 +16,7 @@ use ui::{ActiveTheme as _, Context, ParentElement as _, Styled as _, div}; use util::ResultExt as _; use workspace::{ Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace, - item::{BreadcrumbText, ItemEvent}, + item::{BreadcrumbText, ItemEvent, SaveOptions}, searchable::SearchableItemHandle, }; @@ -386,12 +386,12 @@ impl Item for StackTraceView { fn save( &mut self, - format: bool, + options: SaveOptions, project: Entity, window: &mut Window, cx: &mut Context, ) -> Task> { - self.editor.save(format, project, window, cx) + self.editor.save(options, project, window, cx) } fn save_as( diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index f2165a05ab..bd43ab931e 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -33,6 +33,7 @@ use std::{ use terminal_view::terminal_panel::TerminalPanel; use tests::{active_debug_session_panel, init_test, init_test_workspace}; use util::path; +use workspace::item::SaveOptions; use workspace::{Item, dock::Panel}; #[gpui::test] @@ -1213,7 +1214,15 @@ async fn test_send_breakpoints_when_editor_has_been_saved( editor .update_in(cx, |editor, window, cx| { - editor.save(true, project.clone(), window, cx) + editor.save( + SaveOptions { + format: true, + autosave: false, + }, + project.clone(), + window, + cx, + ) }) .await .unwrap(); diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 955b28d40d..4f66a5a883 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -43,7 +43,7 @@ use ui::{Icon, IconName, Label, h_flex, prelude::*}; use util::ResultExt; use workspace::{ ItemNavHistory, ToolbarItemLocation, Workspace, - item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, + item::{BreadcrumbText, Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams}, searchable::SearchableItemHandle, }; @@ -849,12 +849,12 @@ impl Item for ProjectDiagnosticsEditor { fn save( &mut self, - format: bool, + options: SaveOptions, project: Entity, window: &mut Window, cx: &mut Context, ) -> Task> { - self.editor.save(format, project, window, cx) + self.editor.save(options, project, window, cx) } fn save_as( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 387a5b3972..6bbcebdbc6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -208,7 +208,7 @@ use workspace::{ CollaboratorId, Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal, RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, TabBarSettings, Toast, ViewId, Workspace, WorkspaceId, WorkspaceSettings, - item::{ItemHandle, PreviewTabsSettings}, + item::{ItemHandle, PreviewTabsSettings, SaveOptions}, notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt}, searchable::SearchEvent, }; @@ -1498,7 +1498,7 @@ impl InlayHintRefreshReason { } pub enum FormatTarget { - Buffers, + Buffers(HashSet>), Ranges(Vec>), } @@ -15601,7 +15601,7 @@ impl Editor { Some(self.perform_format( project, FormatTrigger::Manual, - FormatTarget::Buffers, + FormatTarget::Buffers(self.buffer.read(cx).all_buffers()), window, cx, )) @@ -15636,10 +15636,6 @@ impl Editor { )) } - fn save_non_dirty_buffers(&self, cx: &App) -> bool { - self.is_singleton(cx) && EditorSettings::get_global(cx).save_non_dirty_buffers - } - fn perform_format( &mut self, project: Entity, @@ -15650,13 +15646,7 @@ impl Editor { ) -> Task> { let buffer = self.buffer.clone(); let (buffers, target) = match target { - FormatTarget::Buffers => { - let mut buffers = buffer.read(cx).all_buffers(); - if trigger == FormatTrigger::Save && !self.save_non_dirty_buffers(cx) { - buffers.retain(|buffer| buffer.read(cx).is_dirty()); - } - (buffers, LspFormatTarget::Buffers) - } + FormatTarget::Buffers(buffers) => (buffers, LspFormatTarget::Buffers), FormatTarget::Ranges(selection_ranges) => { let multi_buffer = buffer.read(cx); let snapshot = multi_buffer.read(cx); @@ -17101,7 +17091,16 @@ impl Editor { } if let Some(project) = self.project.clone() { - self.save(true, project, window, cx).detach_and_log_err(cx); + self.save( + SaveOptions { + format: true, + autosave: false, + }, + project, + window, + cx, + ) + .detach_and_log_err(cx); } } @@ -17133,7 +17132,16 @@ impl Editor { }); if let Some(project) = self.project.clone() { - self.save(true, project, window, cx).detach_and_log_err(cx); + self.save( + SaveOptions { + format: true, + autosave: false, + }, + project, + window, + cx, + ) + .detach_and_log_err(cx); } } diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index f498e8eb41..44e220f90d 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -50,7 +50,6 @@ pub struct EditorSettings { pub diagnostics_max_severity: Option, pub inline_code_actions: bool, pub drag_and_drop_selection: bool, - pub save_non_dirty_buffers: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -503,12 +502,6 @@ pub struct EditorSettingsContent { /// /// Default: true pub drag_and_drop_selection: Option, - - /// Whether to save singleton buffers that are not dirty. - /// This will "touch" the file and related tools enabled, e.g. formatters. - /// - /// Default: true - pub save_non_dirty_buffers: Option, } // Toolbar related settings diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index e358e2cd71..91ccdfa49d 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -56,7 +56,7 @@ use util::{ }; use workspace::{ CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId, - item::{FollowEvent, FollowableItem, Item, ItemHandle}, + item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions}, }; #[gpui::test] @@ -9041,7 +9041,15 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { ); let save = editor .update_in(cx, |editor, window, cx| { - editor.save(true, project.clone(), window, cx) + editor.save( + SaveOptions { + format: true, + autosave: false, + }, + project.clone(), + window, + cx, + ) }) .unwrap(); cx.executor().start_waiting(); @@ -9073,7 +9081,15 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { ); let save = editor .update_in(cx, |editor, window, cx| { - editor.save(true, project.clone(), window, cx) + editor.save( + SaveOptions { + format: true, + autosave: false, + }, + project.clone(), + window, + cx, + ) }) .unwrap(); cx.executor().advance_clock(super::FORMAT_TIMEOUT); @@ -9085,38 +9101,6 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { ); } - // For non-dirty buffer and the corresponding settings, no formatting request should be sent - { - assert!(!cx.read(|cx| editor.is_dirty(cx))); - cx.update_global::(|settings, cx| { - settings.update_user_settings::(cx, |settings| { - settings.save_non_dirty_buffers = Some(false); - }); - }); - - fake_server.set_request_handler::(move |_, _| async move { - panic!("Should not be invoked on non-dirty buffer when configured so"); - }); - let save = editor - .update_in(cx, |editor, window, cx| { - editor.save(true, project.clone(), window, cx) - }) - .unwrap(); - cx.executor().start_waiting(); - save.await; - - assert_eq!( - editor.update(cx, |editor, cx| editor.text(cx)), - "one\ntwo\nthree\n" - ); - assert!(!cx.read(|cx| editor.is_dirty(cx))); - } - - cx.update_global::(|settings, cx| { - settings.update_user_settings::(cx, |settings| { - settings.save_non_dirty_buffers = Some(false); - }); - }); // Set rust language override and assert overridden tabsize is sent to language server update_test_language_settings(cx, |settings| { settings.languages.insert( @@ -9144,7 +9128,15 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { }); let save = editor .update_in(cx, |editor, window, cx| { - editor.save(true, project.clone(), window, cx) + editor.save( + SaveOptions { + format: true, + autosave: false, + }, + project.clone(), + window, + cx, + ) }) .unwrap(); cx.executor().start_waiting(); @@ -9312,7 +9304,15 @@ async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) { cx.executor().start_waiting(); let save = multi_buffer_editor .update_in(cx, |editor, window, cx| { - editor.save(true, project.clone(), window, cx) + editor.save( + SaveOptions { + format: true, + autosave: false, + }, + project.clone(), + window, + cx, + ) }) .unwrap(); @@ -9356,6 +9356,170 @@ async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/dir"), + json!({ + "file1.rs": "fn main() { println!(\"hello\"); }", + "file2.rs": "fn test() { println!(\"test\"); }", + "file3.rs": "fn other() { println!(\"other\"); }\n", + }), + ) + .await; + + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + + let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap()); + let worktree_id = worktree.update(cx, |worktree, _| worktree.id()); + + // Open three buffers + let buffer_1 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "file1.rs"), cx) + }) + .await + .unwrap(); + let buffer_2 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "file2.rs"), cx) + }) + .await + .unwrap(); + let buffer_3 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "file3.rs"), cx) + }) + .await + .unwrap(); + + // Create a multi-buffer with all three buffers + let multi_buffer = cx.new(|cx| { + let mut multi_buffer = MultiBuffer::new(ReadWrite); + multi_buffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))], + cx, + ); + multi_buffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))], + cx, + ); + multi_buffer.push_excerpts( + buffer_3.clone(), + [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))], + cx, + ); + multi_buffer + }); + + let editor = cx.new_window_entity(|window, cx| { + Editor::new( + EditorMode::full(), + multi_buffer, + Some(project.clone()), + window, + cx, + ) + }); + + // Edit only the first buffer + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(Some(Autoscroll::Next), window, cx, |s| { + s.select_ranges(Some(10..10)) + }); + editor.insert("// edited", window, cx); + }); + + // Verify that only buffer 1 is dirty + buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty())); + buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty())); + buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty())); + + // Get write counts after file creation (files were created with initial content) + // We expect each file to have been written once during creation + let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs")); + let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs")); + let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs")); + + // Perform autosave + let save_task = editor.update_in(cx, |editor, window, cx| { + editor.save( + SaveOptions { + format: true, + autosave: true, + }, + project.clone(), + window, + cx, + ) + }); + save_task.await.unwrap(); + + // Only the dirty buffer should have been saved + assert_eq!( + fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1, + 1, + "Buffer 1 was dirty, so it should have been written once during autosave" + ); + assert_eq!( + fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2, + 0, + "Buffer 2 was clean, so it should not have been written during autosave" + ); + assert_eq!( + fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3, + 0, + "Buffer 3 was clean, so it should not have been written during autosave" + ); + + // Verify buffer states after autosave + buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty())); + buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty())); + buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty())); + + // Now perform a manual save (format = true) + let save_task = editor.update_in(cx, |editor, window, cx| { + editor.save( + SaveOptions { + format: true, + autosave: false, + }, + project.clone(), + window, + cx, + ) + }); + save_task.await.unwrap(); + + // During manual save, clean buffers don't get written to disk + // They just get did_save called for language server notifications + assert_eq!( + fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1, + 1, + "Buffer 1 should only have been written once total (during autosave, not manual save)" + ); + assert_eq!( + fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2, + 0, + "Buffer 2 should not have been written at all" + ); + assert_eq!( + fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3, + 0, + "Buffer 3 should not have been written at all" + ); +} + #[gpui::test] async fn test_range_format_during_save(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -9399,7 +9563,15 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { let save = editor .update_in(cx, |editor, window, cx| { - editor.save(true, project.clone(), window, cx) + editor.save( + SaveOptions { + format: true, + autosave: false, + }, + project.clone(), + window, + cx, + ) }) .unwrap(); fake_server @@ -9442,7 +9614,15 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { ); let save = editor .update_in(cx, |editor, window, cx| { - editor.save(true, project.clone(), window, cx) + editor.save( + SaveOptions { + format: true, + autosave: false, + }, + project.clone(), + window, + cx, + ) }) .unwrap(); cx.executor().advance_clock(super::FORMAT_TIMEOUT); @@ -9454,35 +9634,27 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { ); assert!(!cx.read(|cx| editor.is_dirty(cx))); - // For non-dirty buffer, a formatting request should be sent anyway with the default settings - // where non-dirty singleton buffers are saved and formatted anyway. + // For non-dirty buffer, no formatting request should be sent let save = editor .update_in(cx, |editor, window, cx| { - editor.save(true, project.clone(), window, cx) + editor.save( + SaveOptions { + format: false, + autosave: false, + }, + project.clone(), + window, + cx, + ) }) .unwrap(); - fake_server - .set_request_handler::(move |params, _| async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path(path!("/file.rs")).unwrap() - ); - assert_eq!(params.options.tab_size, 4); - Ok(Some(vec![lsp::TextEdit::new( - lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), - ", ".to_string(), - )])) + let _pending_format_request = fake_server + .set_request_handler::(move |_, _| async move { + panic!("Should not be invoked"); }) - .next() - .await; - cx.executor().advance_clock(super::FORMAT_TIMEOUT); + .next(); cx.executor().start_waiting(); save.await; - assert_eq!( - editor.update(cx, |editor, cx| editor.text(cx)), - "one, two\nthree\n" - ); - assert!(!cx.read(|cx| editor.is_dirty(cx))); // Set Rust language override and assert overridden tabsize is sent to language server update_test_language_settings(cx, |settings| { @@ -9501,7 +9673,15 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { assert!(cx.read(|cx| editor.is_dirty(cx))); let save = editor .update_in(cx, |editor, window, cx| { - editor.save(true, project.clone(), window, cx) + editor.save( + SaveOptions { + format: true, + autosave: false, + }, + project.clone(), + window, + cx, + ) }) .unwrap(); fake_server @@ -9585,7 +9765,7 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { editor.perform_format( project.clone(), FormatTrigger::Manual, - FormatTarget::Buffers, + FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), window, cx, ) @@ -9631,7 +9811,7 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { editor.perform_format( project, FormatTrigger::Manual, - FormatTarget::Buffers, + FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), window, cx, ) @@ -9809,7 +9989,7 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { editor.perform_format( project.clone(), FormatTrigger::Manual, - FormatTarget::Buffers, + FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), window, cx, ) @@ -9845,7 +10025,7 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { editor.perform_format( project.clone(), FormatTrigger::Manual, - FormatTarget::Buffers, + FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), window, cx, ) @@ -15387,7 +15567,7 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) { editor.perform_format( project.clone(), FormatTrigger::Manual, - FormatTarget::Buffers, + FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), window, cx, ) @@ -15407,7 +15587,7 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) { editor.perform_format( project.clone(), FormatTrigger::Manual, - FormatTarget::Buffers, + FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), window, cx, ) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 3a43621594..9e18c40cf4 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -40,7 +40,7 @@ use ui::{IconDecorationKind, prelude::*}; use util::{ResultExt, TryFutureExt, paths::PathExt}; use workspace::{ CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, - item::{FollowableItem, Item, ItemEvent, ProjectItem}, + item::{FollowableItem, Item, ItemEvent, ProjectItem, SaveOptions}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, }; use workspace::{ @@ -805,7 +805,7 @@ impl Item for Editor { fn save( &mut self, - format: bool, + options: SaveOptions, project: Entity, window: &mut Window, cx: &mut Context, @@ -816,48 +816,54 @@ impl Item for Editor { .into_iter() .map(|handle| handle.read(cx).base_buffer().unwrap_or(handle.clone())) .collect::>(); - let save_non_dirty_buffers = self.save_non_dirty_buffers(cx); - cx.spawn_in(window, async move |editor, cx| { - if format { - editor - .update_in(cx, |editor, window, cx| { - editor.perform_format( - project.clone(), - FormatTrigger::Save, - FormatTarget::Buffers, - window, - cx, - ) + + // let mut buffers_to_save = + let buffers_to_save = if self.buffer.read(cx).is_singleton() && !options.autosave { + buffers.clone() + } else { + buffers + .iter() + .filter(|buffer| buffer.read(cx).is_dirty()) + .cloned() + .collect() + }; + + 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, + FormatTarget::Buffers(buffers_to_save.clone()), + window, + cx, + ) + })? + .await?; + } + + if !buffers_to_save.is_empty() { + project + .update(cx, |project, cx| { + project.save_buffers(buffers_to_save.clone(), cx) })? .await?; } - if save_non_dirty_buffers { - project - .update(cx, |project, cx| project.save_buffers(buffers, cx))? - .await?; - } else { - // For multi-buffers, only format and save the buffers with changes. - // For clean buffers, we simulate saving by calling `Buffer::did_save`, - // so that language servers or other downstream listeners of save events get notified. - let (dirty_buffers, clean_buffers) = buffers.into_iter().partition(|buffer| { - buffer - .read_with(cx, |buffer, _| buffer.is_dirty() || buffer.has_conflict()) - .unwrap_or(false) - }); + // Notify about clean buffers for language server events + let buffers_that_were_not_saved: Vec<_> = buffers + .into_iter() + .filter(|b| !buffers_to_save.contains(b)) + .collect(); - project - .update(cx, |project, cx| project.save_buffers(dirty_buffers, cx))? - .await?; - for buffer in clean_buffers { - buffer - .update(cx, |buffer, cx| { - let version = buffer.saved_version().clone(); - let mtime = buffer.saved_mtime(); - buffer.did_save(version, mtime, cx); - }) - .ok(); - } + for buffer in buffers_that_were_not_saved { + buffer + .update(cx, |buffer, cx| { + let version = buffer.saved_version().clone(); + let mtime = buffer.saved_mtime(); + buffer.did_save(version, mtime, cx); + }) + .ok(); } Ok(()) diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index d5ae65d922..06ace151bf 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -12,7 +12,7 @@ use text::ToOffset; use ui::{ButtonLike, KeyBinding, prelude::*}; use workspace::{ Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, - searchable::SearchableItemHandle, + item::SaveOptions, searchable::SearchableItemHandle, }; pub struct ProposedChangesEditor { @@ -351,13 +351,13 @@ impl Item for ProposedChangesEditor { fn save( &mut self, - format: bool, + options: SaveOptions, project: Entity, window: &mut Window, cx: &mut Context, ) -> Task> { self.editor.update(cx, |editor, cx| { - Item::save(editor, format, project, window, cx) + Item::save(editor, options, project, window, cx) }) } } diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 9adbe495dc..75339ede91 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -897,6 +897,7 @@ struct FakeFsState { buffered_events: Vec, metadata_call_count: usize, read_dir_call_count: usize, + path_write_counts: std::collections::HashMap, moves: std::collections::HashMap, home_dir: Option, } @@ -1083,6 +1084,7 @@ impl FakeFs { events_paused: false, read_dir_call_count: 0, metadata_call_count: 0, + path_write_counts: Default::default(), moves: Default::default(), home_dir: None, })), @@ -1173,6 +1175,8 @@ impl FakeFs { recreate_inode: bool, ) -> Result<()> { let mut state = self.state.lock(); + let path_buf = path.as_ref().to_path_buf(); + *state.path_write_counts.entry(path_buf).or_insert(0) += 1; let new_inode = state.get_and_increment_inode(); let new_mtime = state.get_and_increment_mtime(); let new_len = new_content.len() as u64; @@ -1727,6 +1731,17 @@ impl FakeFs { self.state.lock().metadata_call_count } + /// How many write operations have been issued for a specific path. + pub fn write_count_for_path(&self, path: impl AsRef) -> usize { + let path = path.as_ref().to_path_buf(); + self.state + .lock() + .path_write_counts + .get(&path) + .copied() + .unwrap_or(0) + } + fn simulate_random_delay(&self) -> impl futures::Future { self.executor.simulate_random_delay() } diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 1b4346d728..88c0ff71d5 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -37,7 +37,7 @@ use util::ResultExt as _; use workspace::{ CloseActiveItem, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, - item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, + item::{BreadcrumbText, Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams}, searchable::SearchableItemHandle, }; @@ -632,12 +632,12 @@ impl Item for ProjectDiff { fn save( &mut self, - format: bool, + options: SaveOptions, project: Entity, window: &mut Window, cx: &mut Context, ) -> Task> { - self.editor.save(format, project, window, cx) + self.editor.save(options, project, window, cx) } fn save_as( @@ -1565,7 +1565,15 @@ mod tests { cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| { buffer_editor.set_text("different\n", window, cx); - buffer_editor.save(false, project.clone(), window, cx) + buffer_editor.save( + SaveOptions { + format: false, + autosave: false, + }, + project.clone(), + window, + cx, + ) }) .await .unwrap(); diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index 93935148cc..9091feed63 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -15,7 +15,7 @@ use gpui::{ use language::{Language, LanguageRegistry}; use project::{Project, ProjectEntryId, ProjectPath}; use ui::{Tooltip, prelude::*}; -use workspace::item::{ItemEvent, TabContentParams}; +use workspace::item::{ItemEvent, SaveOptions, TabContentParams}; use workspace::searchable::SearchableItemHandle; use workspace::{Item, ItemHandle, Pane, ProjectItem, ToolbarItemLocation}; use workspace::{ToolbarItemEvent, ToolbarItemView}; @@ -782,7 +782,7 @@ impl Item for NotebookEditor { // TODO fn save( &mut self, - _format: bool, + _options: SaveOptions, _project: Entity, _window: &mut Window, _cx: &mut Context, diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 55a24dda6b..9bbf6c73e6 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -41,7 +41,7 @@ use util::{ResultExt as _, paths::PathMatcher}; use workspace::{ DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId, - item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, + item::{BreadcrumbText, Item, ItemEvent, ItemHandle, SaveOptions}, searchable::{Direction, SearchableItem, SearchableItemHandle}, }; @@ -530,13 +530,13 @@ impl Item for ProjectSearchView { fn save( &mut self, - format: bool, + options: SaveOptions, project: Entity, window: &mut Window, cx: &mut Context, ) -> Task> { self.results_editor - .update(cx, |editor, cx| editor.save(format, project, window, cx)) + .update(cx, |editor, cx| editor.save(options, project, window, cx)) } fn save_as( @@ -1086,9 +1086,19 @@ impl ProjectSearchView { let result = result_channel.await?; let should_save = result == 0; if should_save { - this.update_in(cx, |this, window, cx| this.save(true, project, window, cx))? - .await - .log_err(); + this.update_in(cx, |this, window, cx| { + this.save( + SaveOptions { + format: true, + autosave: false, + }, + project, + window, + cx, + ) + })? + .await + .log_err(); } let should_search = result != 2; should_search diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 6b3e1a3911..c8ebe4550b 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -32,6 +32,21 @@ use util::ResultExt; pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200); +#[derive(Clone, Copy, Debug)] +pub struct SaveOptions { + pub format: bool, + pub autosave: bool, +} + +impl Default for SaveOptions { + fn default() -> Self { + Self { + format: true, + autosave: false, + } + } +} + #[derive(Deserialize)] pub struct ItemSettings { pub git_status: bool, @@ -326,7 +341,7 @@ pub trait Item: Focusable + EventEmitter + Render + Sized { } fn save( &mut self, - _format: bool, + _options: SaveOptions, _project: Entity, _window: &mut Window, _cx: &mut Context, @@ -528,7 +543,7 @@ pub trait ItemHandle: 'static + Send { fn can_save_as(&self, cx: &App) -> bool; fn save( &self, - format: bool, + options: SaveOptions, project: Entity, window: &mut Window, cx: &mut App, @@ -991,12 +1006,12 @@ impl ItemHandle for Entity { fn save( &self, - format: bool, + options: SaveOptions, project: Entity, window: &mut Window, cx: &mut App, ) -> Task> { - self.update(cx, |item, cx| item.save(format, project, window, cx)) + self.update(cx, |item, cx| item.save(options, project, window, cx)) } fn save_as( @@ -1305,7 +1320,7 @@ impl WeakFollowableItemHandle for WeakEntity { #[cfg(any(test, feature = "test-support"))] pub mod test { use super::{Item, ItemEvent, SerializableItem, TabContentParams}; - use crate::{ItemId, ItemNavHistory, Workspace, WorkspaceId}; + use crate::{ItemId, ItemNavHistory, Workspace, WorkspaceId, item::SaveOptions}; use gpui::{ AnyElement, App, AppContext as _, Context, Entity, EntityId, EventEmitter, Focusable, InteractiveElement, IntoElement, Render, SharedString, Task, WeakEntity, Window, @@ -1615,7 +1630,7 @@ pub mod test { fn save( &mut self, - _: bool, + _: SaveOptions, _: Entity, _window: &mut Window, cx: &mut Context, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 29d1fd542c..940c9eb04f 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -4,8 +4,8 @@ use crate::{ WorkspaceItemBuilder, item::{ ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings, - ProjectItemKind, ShowCloseButton, ShowDiagnostics, TabContentParams, TabTooltipContent, - WeakItemHandle, + ProjectItemKind, SaveOptions, ShowCloseButton, ShowDiagnostics, TabContentParams, + TabTooltipContent, WeakItemHandle, }, move_item, notifications::NotifyResultExt, @@ -1870,7 +1870,15 @@ impl Pane { match answer.await { Ok(0) => { pane.update_in(cx, |_, window, cx| { - item.save(should_format, project, window, cx) + item.save( + SaveOptions { + format: should_format, + autosave: false, + }, + project, + window, + cx, + ) })? .await? } @@ -1896,7 +1904,15 @@ impl Pane { match answer.await { Ok(0) => { pane.update_in(cx, |_, window, cx| { - item.save(should_format, project, window, cx) + item.save( + SaveOptions { + format: should_format, + autosave: false, + }, + project, + window, + cx, + ) })? .await? } @@ -1967,7 +1983,15 @@ impl Pane { if pane.is_active_preview_item(item.item_id()) { pane.set_preview_item_id(None, cx); } - item.save(should_format, project, window, cx) + item.save( + SaveOptions { + format: should_format, + autosave: false, + }, + project, + window, + cx, + ) })? .await?; } else if can_save_as && is_singleton { @@ -2044,7 +2068,15 @@ impl Pane { AutosaveSetting::AfterDelay { .. } ); if item.can_autosave(cx) { - item.save(format, project, window, cx) + item.save( + SaveOptions { + format, + autosave: true, + }, + project, + window, + cx, + ) } else { Task::ready(Ok(())) } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index c9784d6f15..684af46695 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1764,6 +1764,7 @@ mod tests { use workspace::{ NewFile, OpenOptions, OpenVisible, SERIALIZATION_THROTTLE_TIME, SaveIntent, SplitDirection, WorkspaceHandle, + item::SaveOptions, item::{Item, ItemHandle}, open_new, open_paths, pane, }; @@ -3356,7 +3357,15 @@ mod tests { editor.newline(&Default::default(), window, cx); editor.move_down(&Default::default(), window, cx); editor.move_down(&Default::default(), window, cx); - editor.save(true, project.clone(), window, cx) + editor.save( + SaveOptions { + format: true, + autosave: false, + }, + project.clone(), + window, + cx, + ) }) }) .unwrap()