Don't autosave unmodified buffers (#32626)

Closes https://github.com/zed-industries/zed/issues/12091

Proper redo of https://github.com/zed-industries/zed/pull/32603

Release Notes:

- Fixed formatting effects not triggered when saving unmodified
singleton buffers

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Cole Miller <m@cole-miller.net>
This commit is contained in:
Kirill Bulatov 2025-06-13 01:12:14 +03:00 committed by GitHub
parent cd018da1ad
commit cef0c415f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 453 additions and 171 deletions

View file

@ -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`

View file

@ -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<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.editor.save(format, project, window, cx)
self.editor.save(options, project, window, cx)
}
fn save_as(

View file

@ -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<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.editor.save(format, project, window, cx)
self.editor.save(options, project, window, cx)
}
fn save_as(

View file

@ -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();

View file

@ -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<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.editor.save(format, project, window, cx)
self.editor.save(options, project, window, cx)
}
fn save_as(

View file

@ -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<Entity<Buffer>>),
Ranges(Vec<Range<MultiBufferPoint>>),
}
@ -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<Project>,
@ -15650,13 +15646,7 @@ impl Editor {
) -> Task<Result<()>> {
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);
}
}

View file

@ -50,7 +50,6 @@ pub struct EditorSettings {
pub diagnostics_max_severity: Option<DiagnosticSeverity>,
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<bool>,
/// 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<bool>,
}
// Toolbar related settings

View file

@ -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::<SettingsStore, _>(|settings, cx| {
settings.update_user_settings::<EditorSettings>(cx, |settings| {
settings.save_non_dirty_buffers = Some(false);
});
});
fake_server.set_request_handler::<lsp::request::Formatting, _, _>(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::<SettingsStore, _>(|settings, cx| {
settings.update_user_settings::<EditorSettings>(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::<lsp::request::RangeFormatting, _, _>(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::<lsp::request::RangeFormatting, _, _>(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,
)

View file

@ -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<Project>,
window: &mut Window,
cx: &mut Context<Self>,
@ -816,48 +816,54 @@ impl Item for Editor {
.into_iter()
.map(|handle| handle.read(cx).base_buffer().unwrap_or(handle.clone()))
.collect::<HashSet<_>>();
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(())

View file

@ -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<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<anyhow::Result<()>> {
self.editor.update(cx, |editor, cx| {
Item::save(editor, format, project, window, cx)
Item::save(editor, options, project, window, cx)
})
}
}

View file

@ -897,6 +897,7 @@ struct FakeFsState {
buffered_events: Vec<PathEvent>,
metadata_call_count: usize,
read_dir_call_count: usize,
path_write_counts: std::collections::HashMap<PathBuf, usize>,
moves: std::collections::HashMap<u64, PathBuf>,
home_dir: Option<PathBuf>,
}
@ -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<Path>) -> 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<Output = ()> {
self.executor.simulate_random_delay()
}

View file

@ -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<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
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();

View file

@ -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<Project>,
_window: &mut Window,
_cx: &mut Context<Self>,

View file

@ -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<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<anyhow::Result<()>> {
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

View file

@ -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<Self::Event> + Render + Sized {
}
fn save(
&mut self,
_format: bool,
_options: SaveOptions,
_project: Entity<Project>,
_window: &mut Window,
_cx: &mut Context<Self>,
@ -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<Project>,
window: &mut Window,
cx: &mut App,
@ -991,12 +1006,12 @@ impl<T: Item> ItemHandle for Entity<T> {
fn save(
&self,
format: bool,
options: SaveOptions,
project: Entity<Project>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<()>> {
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<T: FollowableItem> WeakFollowableItemHandle for WeakEntity<T> {
#[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<Project>,
_window: &mut Window,
cx: &mut Context<Self>,

View file

@ -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(()))
}

View file

@ -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()