Detailed changes
@@ -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`
@@ -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(
@@ -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(
@@ -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();
@@ -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(
@@ -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);
}
}
@@ -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
@@ -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,
)
@@ -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,
- )
- })?
- .await?;
+
+ // 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 save_non_dirty_buffers {
+ if !buffers_to_save.is_empty() {
project
- .update(cx, |project, cx| project.save_buffers(buffers, cx))?
+ .update(cx, |project, cx| {
+ project.save_buffers(buffers_to_save.clone(), 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)
- });
+ }
- 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();
- }
+ // 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();
+
+ 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(())
@@ -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)
})
}
}
@@ -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()
}
@@ -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();
@@ -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>,
@@ -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
@@ -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>,
@@ -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(()))
}
@@ -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()