Add a setting for ensuring a single final newline on save

This commit is contained in:
Max Brunsfeld 2023-02-27 14:32:08 -08:00
parent 7faa0da5c7
commit a890b8f3b7
5 changed files with 88 additions and 20 deletions

View file

@ -51,8 +51,12 @@
// 3. Position the dock full screen over the entire workspace" // 3. Position the dock full screen over the entire workspace"
// "default_dock_anchor": "expanded" // "default_dock_anchor": "expanded"
"default_dock_anchor": "right", "default_dock_anchor": "right",
// Whether or not to remove trailing whitespace from lines before saving. // Whether or not to remove any trailing whitespace from lines of a buffer
// before saving it.
"remove_trailing_whitespace_on_save": true, "remove_trailing_whitespace_on_save": true,
// Whether or not to ensure there's a single newline at the end of a buffer
// when saving it.
"ensure_final_newline_on_save": true,
// Whether or not to perform a buffer format before saving // Whether or not to perform a buffer format before saving
"format_on_save": "on", "format_on_save": "on",
// How to perform a buffer format. This setting can take two values: // How to perform a buffer format. This setting can take two values:

View file

@ -4314,12 +4314,13 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
) )
.await; .await;
// Set up a buffer white some trailing whitespace. // Set up a buffer white some trailing whitespace and no trailing newline.
cx.set_state( cx.set_state(
&[ &[
"one ", // "one ", //
"twoˇ", // "twoˇ", //
"three ", // "three ", //
"four", //
] ]
.join("\n"), .join("\n"),
); );
@ -4348,7 +4349,8 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
cx.lsp.handle_request::<lsp::request::Formatting, _, _>({ cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
let buffer_changes = buffer_changes.clone(); let buffer_changes = buffer_changes.clone();
move |_, _| { move |_, _| {
// When formatting is requested, trailing whitespace has already been stripped. // When formatting is requested, trailing whitespace has already been stripped,
// and the trailing newline has already been added.
assert_eq!( assert_eq!(
&buffer_changes.lock()[1..], &buffer_changes.lock()[1..],
&[ &[
@ -4360,6 +4362,10 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)), lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
"".into() "".into()
), ),
(
lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
"\n".into()
),
] ]
); );
@ -4379,8 +4385,9 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
} }
}); });
// After formatting the buffer, the trailing whitespace is stripped and the // After formatting the buffer, the trailing whitespace is stripped,
// edits provided by the language server have been applied. // a newline is appended, and the edits provided by the language server
// have been applied.
format.await.unwrap(); format.await.unwrap();
cx.assert_editor_state( cx.assert_editor_state(
&[ &[
@ -4389,18 +4396,21 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
"twoˇ", // "twoˇ", //
"", // "", //
"three", // "three", //
"four", //
"", //
] ]
.join("\n"), .join("\n"),
); );
// Undoing the formatting undoes both the trailing whitespace removal and the // Undoing the formatting undoes the trailing whitespace removal, the
// LSP edits. // trailing newline, and the LSP edits.
cx.update_buffer(|buffer, cx| buffer.undo(cx)); cx.update_buffer(|buffer, cx| buffer.undo(cx));
cx.assert_editor_state( cx.assert_editor_state(
&[ &[
"one ", // "one ", //
"twoˇ", // "twoˇ", //
"three ", // "three ", //
"four", //
] ]
.join("\n"), .join("\n"),
); );

View file

@ -1156,6 +1156,8 @@ impl Buffer {
}) })
} }
/// Spawn a background task that searches the buffer for any whitespace
/// at the ends of a lines, and returns a `Diff` that removes that whitespace.
pub fn remove_trailing_whitespace(&self, cx: &AppContext) -> Task<Diff> { pub fn remove_trailing_whitespace(&self, cx: &AppContext) -> Task<Diff> {
let old_text = self.as_rope().clone(); let old_text = self.as_rope().clone();
let line_ending = self.line_ending(); let line_ending = self.line_ending();
@ -1174,6 +1176,27 @@ impl Buffer {
}) })
} }
/// Ensure that the buffer ends with a single newline character, and
/// no other whitespace.
pub fn ensure_final_newline(&mut self, cx: &mut ModelContext<Self>) {
let len = self.len();
let mut offset = len;
for chunk in self.as_rope().reversed_chunks_in_range(0..len) {
let non_whitespace_len = chunk
.trim_end_matches(|c: char| c.is_ascii_whitespace())
.len();
offset -= chunk.len();
offset += non_whitespace_len;
if non_whitespace_len != 0 {
if offset == len - 1 && chunk.get(non_whitespace_len..) == Some("\n") {
return;
}
break;
}
}
self.edit([(offset..len, "\n")], None, cx);
}
pub fn apply_diff(&mut self, diff: Diff, cx: &mut ModelContext<Self>) -> Option<TransactionId> { pub fn apply_diff(&mut self, diff: Diff, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
if self.version == diff.base_version { if self.version == diff.base_version {
self.apply_non_conflicting_portion_of_diff(diff, cx) self.apply_non_conflicting_portion_of_diff(diff, cx)
@ -1182,6 +1205,9 @@ impl Buffer {
} }
} }
/// Apply a diff to the buffer. If the buffer has changed since the given diff was
/// calculated, then adjust the diff to account for those changes, and discard any
/// parts of the diff that conflict with those changes.
pub fn apply_non_conflicting_portion_of_diff( pub fn apply_non_conflicting_portion_of_diff(
&mut self, &mut self,
diff: Diff, diff: Diff,

View file

@ -2887,18 +2887,23 @@ impl Project {
let mut project_transaction = ProjectTransaction::default(); let mut project_transaction = ProjectTransaction::default();
for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers { for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers {
let (format_on_save, remove_trailing_whitespace, formatter, tab_size) = buffer let (
.read_with(&cx, |buffer, cx| { format_on_save,
let settings = cx.global::<Settings>(); remove_trailing_whitespace,
let language_name = buffer.language().map(|language| language.name()); ensure_final_newline,
( formatter,
settings.format_on_save(language_name.as_deref()), tab_size,
settings ) = buffer.read_with(&cx, |buffer, cx| {
.remove_trailing_whitespace_on_save(language_name.as_deref()), let settings = cx.global::<Settings>();
settings.formatter(language_name.as_deref()), let language_name = buffer.language().map(|language| language.name());
settings.tab_size(language_name.as_deref()), (
) settings.format_on_save(language_name.as_deref()),
}); settings.remove_trailing_whitespace_on_save(language_name.as_deref()),
settings.ensure_final_newline_on_save(language_name.as_deref()),
settings.formatter(language_name.as_deref()),
settings.tab_size(language_name.as_deref()),
)
});
let whitespace_transaction_id = if remove_trailing_whitespace { let whitespace_transaction_id = if remove_trailing_whitespace {
let diff = buffer let diff = buffer
@ -2906,7 +2911,19 @@ impl Project {
.await; .await;
buffer.update(&mut cx, move |buffer, cx| { buffer.update(&mut cx, move |buffer, cx| {
buffer.finalize_last_transaction(); buffer.finalize_last_transaction();
buffer.apply_non_conflicting_portion_of_diff(diff, cx) buffer.start_transaction();
buffer.apply_non_conflicting_portion_of_diff(diff, cx);
if ensure_final_newline {
buffer.ensure_final_newline(cx);
}
buffer.end_transaction(cx)
})
} else if ensure_final_newline {
buffer.update(&mut cx, move |buffer, cx| {
buffer.finalize_last_transaction();
buffer.start_transaction();
buffer.ensure_final_newline(cx);
buffer.end_transaction(cx)
}) })
} else { } else {
None None

View file

@ -95,6 +95,7 @@ pub struct EditorSettings {
pub preferred_line_length: Option<u32>, pub preferred_line_length: Option<u32>,
pub format_on_save: Option<FormatOnSave>, pub format_on_save: Option<FormatOnSave>,
pub remove_trailing_whitespace_on_save: Option<bool>, pub remove_trailing_whitespace_on_save: Option<bool>,
pub ensure_final_newline_on_save: Option<bool>,
pub formatter: Option<Formatter>, pub formatter: Option<Formatter>,
pub enable_language_server: Option<bool>, pub enable_language_server: Option<bool>,
} }
@ -365,6 +366,9 @@ impl Settings {
remove_trailing_whitespace_on_save: required( remove_trailing_whitespace_on_save: required(
defaults.editor.remove_trailing_whitespace_on_save, defaults.editor.remove_trailing_whitespace_on_save,
), ),
ensure_final_newline_on_save: required(
defaults.editor.ensure_final_newline_on_save,
),
format_on_save: required(defaults.editor.format_on_save), format_on_save: required(defaults.editor.format_on_save),
formatter: required(defaults.editor.formatter), formatter: required(defaults.editor.formatter),
enable_language_server: required(defaults.editor.enable_language_server), enable_language_server: required(defaults.editor.enable_language_server),
@ -470,6 +474,12 @@ impl Settings {
}) })
} }
pub fn ensure_final_newline_on_save(&self, language: Option<&str>) -> bool {
self.language_setting(language, |settings| {
settings.ensure_final_newline_on_save.clone()
})
}
pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave { pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave {
self.language_setting(language, |settings| settings.format_on_save.clone()) self.language_setting(language, |settings| settings.format_on_save.clone())
} }
@ -569,6 +579,7 @@ impl Settings {
soft_wrap: Some(SoftWrap::None), soft_wrap: Some(SoftWrap::None),
preferred_line_length: Some(80), preferred_line_length: Some(80),
remove_trailing_whitespace_on_save: Some(true), remove_trailing_whitespace_on_save: Some(true),
ensure_final_newline_on_save: Some(true),
format_on_save: Some(FormatOnSave::On), format_on_save: Some(FormatOnSave::On),
formatter: Some(Formatter::LanguageServer), formatter: Some(Formatter::LanguageServer),
enable_language_server: Some(true), enable_language_server: Some(true),