Implement 'format without save' (#8806)

This solves a major usability problem in Zed, that there's no way to
temporarily disable auto formatting without toggling the whole feature
off.

fixes https://github.com/zed-industries/zed/issues/5230

Release Notes:

- Added a new `workspace::SaveWithoutFormatting`, bound to `cmd-k s`, to
save a file without invoking the auto formatter.
This commit is contained in:
Mikayla Maki 2024-03-03 21:47:34 -08:00 committed by GitHub
parent ff65008316
commit 20acc123af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 69 additions and 24 deletions

View file

@ -365,6 +365,7 @@
"ctrl-alt-b": "branches::OpenRecent",
"ctrl-~": "workspace::NewTerminal",
"ctrl-s": "workspace::Save",
"ctrl-k s": "workspace::SaveWithoutFormat",
"ctrl-shift-s": "workspace::SaveAs",
"ctrl-n": "workspace::NewFile",
"ctrl-shift-n": "workspace::NewWindow",

View file

@ -408,6 +408,7 @@
"alt-cmd-b": "branches::OpenRecent",
"ctrl-~": "workspace::NewTerminal",
"cmd-s": "workspace::Save",
"cmd-k s": "workspace::SaveWithoutFormat",
"cmd-shift-s": "workspace::SaveAs",
"cmd-n": "workspace::NewFile",
"cmd-shift-n": "workspace::NewWindow",
@ -426,8 +427,8 @@
"cmd-j": "workspace::ToggleBottomDock",
"alt-cmd-y": "workspace::CloseAllDocks",
"cmd-shift-f": "pane::DeploySearch",
"cmd-k cmd-t": "theme_selector::Toggle",
"cmd-k cmd-s": "zed::OpenKeymap",
"cmd-k cmd-t": "theme_selector::Toggle",
"cmd-t": "project_symbols::Toggle",
"cmd-p": "file_finder::Toggle",
"cmd-shift-p": "command_palette::Toggle",

View file

@ -735,8 +735,13 @@ impl Item for ProjectDiagnosticsEditor {
true
}
fn save(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
self.editor.save(project, cx)
fn save(
&mut self,
format: bool,
project: Model<Project>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.editor.save(format, project, cx)
}
fn save_as(

View file

@ -5265,7 +5265,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
assert!(cx.read(|cx| editor.is_dirty(cx)));
let save = editor
.update(cx, |editor, cx| editor.save(project.clone(), cx))
.update(cx, |editor, cx| editor.save(true, project.clone(), cx))
.unwrap();
fake_server
.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
@ -5303,7 +5303,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
unreachable!()
});
let save = editor
.update(cx, |editor, cx| editor.save(project.clone(), cx))
.update(cx, |editor, cx| editor.save(true, project.clone(), cx))
.unwrap();
cx.executor().advance_clock(super::FORMAT_TIMEOUT);
cx.executor().start_waiting();
@ -5326,7 +5326,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
});
let save = editor
.update(cx, |editor, cx| editor.save(project.clone(), cx))
.update(cx, |editor, cx| editor.save(true, project.clone(), cx))
.unwrap();
fake_server
.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
@ -5379,7 +5379,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
assert!(cx.read(|cx| editor.is_dirty(cx)));
let save = editor
.update(cx, |editor, cx| editor.save(project.clone(), cx))
.update(cx, |editor, cx| editor.save(true, project.clone(), cx))
.unwrap();
fake_server
.handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
@ -5418,7 +5418,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
},
);
let save = editor
.update(cx, |editor, cx| editor.save(project.clone(), cx))
.update(cx, |editor, cx| editor.save(true, project.clone(), cx))
.unwrap();
cx.executor().advance_clock(super::FORMAT_TIMEOUT);
cx.executor().start_waiting();
@ -5441,7 +5441,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
});
let save = editor
.update(cx, |editor, cx| editor.save(project.clone(), cx))
.update(cx, |editor, cx| editor.save(true, project.clone(), cx))
.unwrap();
fake_server
.handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {

View file

@ -702,14 +702,21 @@ impl Item for Editor {
}
}
fn save(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
fn save(
&mut self,
format: bool,
project: Model<Project>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.report_editor_event("save", None, cx);
let buffers = self.buffer().clone().read(cx).all_buffers();
cx.spawn(|this, mut cx| async move {
if format {
this.update(&mut cx, |this, cx| {
this.perform_format(project.clone(), FormatTrigger::Save, cx)
})?
.await?;
}
if buffers.len() == 1 {
project

View file

@ -539,11 +539,12 @@ impl Item for ProjectSearchView {
fn save(
&mut self,
format: bool,
project: Model<Project>,
cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
self.results_editor
.update(cx, |editor, cx| editor.save(project, cx))
.update(cx, |editor, cx| editor.save(format, project, cx))
}
fn save_as(

View file

@ -146,7 +146,12 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
fn can_save(&self, _cx: &AppContext) -> bool {
false
}
fn save(&mut self, _project: Model<Project>, _cx: &mut ViewContext<Self>) -> Task<Result<()>> {
fn save(
&mut self,
_format: bool,
_project: Model<Project>,
_cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
unimplemented!("save() must be implemented if can_save() returns true")
}
fn save_as(
@ -258,7 +263,12 @@ pub trait ItemHandle: 'static + Send {
fn is_dirty(&self, cx: &AppContext) -> bool;
fn has_conflict(&self, cx: &AppContext) -> bool;
fn can_save(&self, cx: &AppContext) -> bool;
fn save(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>>;
fn save(
&self,
format: bool,
project: Model<Project>,
cx: &mut WindowContext,
) -> Task<Result<()>>;
fn save_as(
&self,
project: Model<Project>,
@ -566,8 +576,13 @@ impl<T: Item> ItemHandle for View<T> {
self.read(cx).can_save(cx)
}
fn save(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>> {
self.update(cx, |item, cx| item.save(project, cx))
fn save(
&self,
format: bool,
project: Model<Project>,
cx: &mut WindowContext,
) -> Task<Result<()>> {
self.update(cx, |item, cx| item.save(format, project, cx))
}
fn save_as(
@ -1018,6 +1033,7 @@ pub mod test {
fn save(
&mut self,
_: bool,
_: Model<Project>,
_: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {

View file

@ -44,6 +44,8 @@ pub enum SaveIntent {
/// write all files (even if unchanged)
/// prompt before overwriting on-disk changes
Save,
/// same as Save, but without auto formatting
SaveWithoutFormat,
/// write any files that have local changes
/// prompt before overwriting on-disk changes
SaveAll,
@ -1122,7 +1124,7 @@ impl Pane {
})?;
// when saving a single buffer, we ignore whether or not it's dirty.
if save_intent == SaveIntent::Save {
if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
is_dirty = true;
}
@ -1136,6 +1138,8 @@ impl Pane {
has_conflict = false;
}
let should_format = save_intent != SaveIntent::SaveWithoutFormat;
if has_conflict && can_save {
let answer = pane.update(cx, |pane, cx| {
pane.activate_item(item_ix, true, true, cx);
@ -1147,7 +1151,10 @@ impl Pane {
)
})?;
match answer.await {
Ok(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
Ok(0) => {
pane.update(cx, |_, cx| item.save(should_format, project, cx))?
.await?
}
Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
_ => return Ok(false),
}
@ -1179,7 +1186,8 @@ impl Pane {
}
if can_save {
pane.update(cx, |_, cx| item.save(project, cx))?.await?;
pane.update(cx, |_, cx| item.save(should_format, project, cx))?
.await?;
} else if can_save_as {
let start_abs_path = project
.update(cx, |project, cx| {
@ -1211,7 +1219,7 @@ impl Pane {
cx: &mut WindowContext,
) -> Task<Result<()>> {
if Self::can_autosave_item(item, cx) {
item.save(project, cx)
item.save(true, project, cx)
} else {
Task::ready(Ok(()))
}

View file

@ -106,6 +106,7 @@ actions!(
AddFolderToProject,
Unfollow,
SaveAs,
SaveWithoutFormat,
ReloadActiveItem,
ActivatePreviousPane,
ActivateNextPane,
@ -3532,6 +3533,11 @@ impl Workspace {
.save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
.detach_and_log_err(cx);
}))
.on_action(cx.listener(|workspace, _: &SaveWithoutFormat, cx| {
workspace
.save_active_item(SaveIntent::SaveWithoutFormat, cx)
.detach_and_log_err(cx);
}))
.on_action(cx.listener(|workspace, _: &SaveAs, cx| {
workspace
.save_active_item(SaveIntent::SaveAs, cx)

View file

@ -1979,7 +1979,7 @@ mod tests {
editor.newline(&Default::default(), cx);
editor.move_down(&Default::default(), cx);
editor.move_down(&Default::default(), cx);
editor.save(project.clone(), cx)
editor.save(true, project.clone(), cx)
})
})
.unwrap()