Unnamed repository; edit this file 'description' to name the repository.
Add config for trimming trailing whitespace and newlines on write
These match the equivalent options in VSCode. `trim_trailing_whitespace` is also the name used by EditorConfig. * `trim-final-newlines` trims any extra line endings after the final one * `trim-trailing-whitespace` trims any trailing whitespace (but not empty lines)
Michael Davis 12 months ago
parent ee9db44 · commit aa20eb8
-rw-r--r--book/src/editor.md2
-rw-r--r--helix-term/src/commands/typed.rs62
-rw-r--r--helix-term/tests/test/commands/write.rs44
-rw-r--r--helix-view/src/editor.rs8
4 files changed, 116 insertions, 0 deletions
diff --git a/book/src/editor.md b/book/src/editor.md
index 2baa907f..79f7284c 100644
--- a/book/src/editor.md
+++ b/book/src/editor.md
@@ -53,6 +53,8 @@
| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` |
| `default-line-ending` | The line ending to use for new documents. Can be `native`, `lf`, `crlf`, `ff`, `cr` or `nel`. `native` uses the platform's native line ending (`crlf` on Windows, otherwise `lf`). | `native` |
| `insert-final-newline` | Whether to automatically insert a trailing line-ending on write if missing | `true` |
+| `trim-final-newlines` | Whether to automatically remove line-endings after the final one on write | `false` |
+| `trim-trailing-whitespace` | Whether to automatically remove whitespace preceding line endings on write | `false` |
| `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` |
| `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid`
| `jump-label-alphabet` | The characters that are used to generate two character jump labels. Characters at the start of the alphabet are used first. | `"abcdefghijklmnopqrstuvwxyz"`
diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs
index 1d57930c..07374f77 100644
--- a/helix-term/src/commands/typed.rs
+++ b/helix-term/src/commands/typed.rs
@@ -326,6 +326,12 @@ fn write_impl(cx: &mut compositor::Context, path: Option<&str>, force: bool) ->
let jobs = &mut cx.jobs;
let (view, doc) = current!(cx.editor);
+ if config.trim_trailing_whitespace {
+ trim_trailing_whitespace(doc, view.id);
+ }
+ if config.trim_final_newlines {
+ trim_final_newlines(doc, view.id);
+ }
if config.insert_final_newline {
insert_final_newline(doc, view.id);
}
@@ -357,6 +363,56 @@ fn write_impl(cx: &mut compositor::Context, path: Option<&str>, force: bool) ->
Ok(())
}
+/// Trim all whitespace preceding line-endings in a document.
+fn trim_trailing_whitespace(doc: &mut Document, view_id: ViewId) {
+ let text = doc.text();
+ let mut pos = 0;
+ let transaction = Transaction::delete(
+ text,
+ text.lines().filter_map(|line| {
+ let line_end_len_chars = line_ending::get_line_ending(&line)
+ .map(|le| le.len_chars())
+ .unwrap_or_default();
+ // Char after the last non-whitespace character or the beginning of the line if the
+ // line is all whitespace:
+ let first_trailing_whitespace =
+ pos + line.last_non_whitespace_char().map_or(0, |idx| idx + 1);
+ pos += line.len_chars();
+ // Char before the line ending character(s), or the final char in the text if there
+ // is no line-ending on this line:
+ let line_end = pos - line_end_len_chars;
+ if first_trailing_whitespace != line_end {
+ Some((first_trailing_whitespace, line_end))
+ } else {
+ None
+ }
+ }),
+ );
+ doc.apply(&transaction, view_id);
+}
+
+/// Trim any extra line-endings after the final line-ending.
+fn trim_final_newlines(doc: &mut Document, view_id: ViewId) {
+ let rope = doc.text();
+ let mut text = rope.slice(..);
+ let mut total_char_len = 0;
+ let mut final_char_len = 0;
+ while let Some(line_ending) = line_ending::get_line_ending(&text) {
+ total_char_len += line_ending.len_chars();
+ final_char_len = line_ending.len_chars();
+ text = text.slice(..text.len_chars() - line_ending.len_chars());
+ }
+ let chars_to_delete = total_char_len - final_char_len;
+ if chars_to_delete != 0 {
+ let transaction = Transaction::delete(
+ rope,
+ [(rope.len_chars() - chars_to_delete, rope.len_chars())].into_iter(),
+ );
+ doc.apply(&transaction, view_id);
+ }
+}
+
+/// Ensure that the document is terminated with a line ending.
fn insert_final_newline(doc: &mut Document, view_id: ViewId) {
let text = doc.text();
if line_ending::get_line_ending(&text.slice(..)).is_none() {
@@ -682,6 +738,12 @@ pub fn write_all_impl(
let doc = doc_mut!(cx.editor, &doc_id);
let view = view_mut!(cx.editor, target_view);
+ if config.trim_trailing_whitespace {
+ trim_trailing_whitespace(doc, target_view);
+ }
+ if config.trim_final_newlines {
+ trim_final_newlines(doc, target_view);
+ }
if config.insert_final_newline {
insert_final_newline(doc, target_view);
}
diff --git a/helix-term/tests/test/commands/write.rs b/helix-term/tests/test/commands/write.rs
index aba101e9..38ab643c 100644
--- a/helix-term/tests/test/commands/write.rs
+++ b/helix-term/tests/test/commands/write.rs
@@ -421,6 +421,50 @@ async fn test_write_utf_bom_file() -> anyhow::Result<()> {
}
#[tokio::test(flavor = "multi_thread")]
+async fn test_write_trim_trailing_whitespace() -> anyhow::Result<()> {
+ let mut file = tempfile::NamedTempFile::new()?;
+ let mut app = helpers::AppBuilder::new()
+ .with_config(Config {
+ editor: helix_view::editor::Config {
+ trim_trailing_whitespace: true,
+ ..Default::default()
+ },
+ ..Default::default()
+ })
+ .with_file(file.path(), None)
+ .with_input_text("#[f|]#oo \n\n \nbar ")
+ .build()?;
+
+ test_key_sequence(&mut app, Some(":w<ret>"), None, false).await?;
+
+ helpers::assert_file_has_content(&mut file, &LineFeedHandling::Native.apply("foo\n\n\nbar"))?;
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn test_write_trim_final_newlines() -> anyhow::Result<()> {
+ let mut file = tempfile::NamedTempFile::new()?;
+ let mut app = helpers::AppBuilder::new()
+ .with_config(Config {
+ editor: helix_view::editor::Config {
+ trim_final_newlines: true,
+ ..Default::default()
+ },
+ ..Default::default()
+ })
+ .with_file(file.path(), None)
+ .with_input_text("#[f|]#oo\n \n\n\n")
+ .build()?;
+
+ test_key_sequence(&mut app, Some(":w<ret>"), None, false).await?;
+
+ helpers::assert_file_has_content(&mut file, &LineFeedHandling::Native.apply("foo\n \n"))?;
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread")]
async fn test_write_insert_final_newline_added_if_missing() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?;
let mut app = helpers::AppBuilder::new()
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index 739dcfb4..cdc48a54 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -342,6 +342,12 @@ pub struct Config {
pub default_line_ending: LineEndingConfig,
/// Whether to automatically insert a trailing line-ending on write if missing. Defaults to `true`.
pub insert_final_newline: bool,
+ /// Whether to automatically remove all trailing line-endings after the final one on write.
+ /// Defaults to `false`.
+ pub trim_final_newlines: bool,
+ /// Whether to automatically remove all whitespace characters preceding line-endings on write.
+ /// Defaults to `false`.
+ pub trim_trailing_whitespace: bool,
/// Enables smart tab
pub smart_tab: Option<SmartTabConfig>,
/// Draw border around popups.
@@ -994,6 +1000,8 @@ impl Default for Config {
workspace_lsp_roots: Vec::new(),
default_line_ending: LineEndingConfig::default(),
insert_final_newline: true,
+ trim_final_newlines: false,
+ trim_trailing_whitespace: false,
smart_tab: Some(SmartTabConfig::default()),
popup_border: PopupBorderConfig::None,
indent_heuristic: IndentationHeuristic::default(),