From cb68afbd2be66dc6f9fef592dabf7c56a9a92d3d Mon Sep 17 00:00:00 2001 From: Mohit Goyal Date: Wed, 22 Jul 2026 21:10:32 +0000 Subject: [PATCH] Respect disabled trailing whitespace removal (#58776) Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #58686 Release Notes: - Fixed `.editorconfig` re-enabling trailing whitespace removal when Zed settings disable it. --------- Co-authored-by: Martin Ye --- crates/editor/src/editor_tests.rs | 70 +++++++++++++++++++ crates/language/src/language_settings.rs | 12 +++- .../tests/integration/project_tests.rs | 24 ++++++- 3 files changed, 102 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index d7c68d6399..18c56ef03a 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -16,6 +16,7 @@ use crate::{ }; use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind}; use collections::HashMap; +use fs::Fs as _; use futures::{StreamExt, channel::oneshot}; use gpui::{ BackgroundExecutor, DismissEvent, Task, TaskExt, TestAppContext, UpdateGlobal, @@ -17603,6 +17604,75 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_markdown_save_respects_disabled_trailing_whitespace_removal(cx: &mut TestAppContext) { + init_test(cx, |settings| { + settings.defaults.ensure_final_newline_on_save = Some(false); + settings.languages.0.insert( + "Markdown".into(), + LanguageSettingsContent { + remove_trailing_whitespace_on_save: Some(false), + ..Default::default() + }, + ); + }); + + let fs = FakeFs::new(cx.executor()); + fs.insert_file( + path!("/.editorconfig"), + "root = true\n[*]\ntrim_trailing_whitespace = true\n".into(), + ) + .await; + fs.insert_file(path!("/README.md"), "".into()).await; + + let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(markdown_lang()); + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer(path!("/README.md"), cx) + }) + .await + .unwrap(); + + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| { + build_editor_with_project(project.clone(), buffer, window, cx) + }); + editor.update_in(cx, |editor, window, cx| { + editor.set_text("line with spaces \n", window, cx) + }); + + let save = editor + .update_in(cx, |editor, window, cx| { + editor.save( + SaveOptions { + format: true, + force_format: false, + autosave: false, + }, + project.clone(), + window, + cx, + ) + }) + .unwrap(); + save.await; + + assert_eq!( + editor.update(cx, |editor, cx| editor.text(cx)), + "line with spaces \n" + ); + assert_eq!( + fs.load(path!("/README.md").as_ref()) + .await + .unwrap() + .replace("\r\n", "\n"), + "line with spaces \n" + ); +} + #[gpui::test] async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) { init_test(cx, |settings| { diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 1cf70338f4..0b5c68a4af 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -695,9 +695,15 @@ fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigPr .merge_from_option(preferred_line_length.as_ref()); settings.tab_size.merge_from_option(tab_size.as_ref()); settings.hard_tabs.merge_from_option(hard_tabs.as_ref()); - settings - .remove_trailing_whitespace_on_save - .merge_from_option(remove_trailing_whitespace_on_save.as_ref()); + // Avoid re-enabling destructive whitespace trimming when Zed settings have + // disabled it, e.g. for Markdown hard breaks. + if !matches!(remove_trailing_whitespace_on_save, Some(true)) + || settings.remove_trailing_whitespace_on_save + { + settings + .remove_trailing_whitespace_on_save + .merge_from_option(remove_trailing_whitespace_on_save.as_ref()); + } settings .ensure_final_newline_on_save .merge_from_option(ensure_final_newline_on_save.as_ref()); diff --git a/crates/project/tests/integration/project_tests.rs b/crates/project/tests/integration/project_tests.rs index e4796b29a1..f6f8ac8eab 100644 --- a/crates/project/tests/integration/project_tests.rs +++ b/crates/project/tests/integration/project_tests.rs @@ -254,7 +254,10 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { max_line_length = 120 [*.js] tab_width = 10 + trim_trailing_whitespace = false max_line_length = off + [*.json] + trim_trailing_whitespace = true "#, ".zed": { "settings.json": r#"{ @@ -264,6 +267,17 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { "remove_trailing_whitespace_on_save": false, "preferred_line_length": 64, "soft_wrap": "editor_width", + "languages": { + "JavaScript": { + "remove_trailing_whitespace_on_save": true, + }, + "JSON": { + "remove_trailing_whitespace_on_save": true, + }, + "Markdown": { + "remove_trailing_whitespace_on_save": true, + }, + }, }"#, }, "a.rs": "fn a() {\n A\n}", @@ -293,6 +307,7 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { "e.rs": "fn e() {\n E\n}", }, "README.json": "tabs are better\n", + "README.md": "spaces are meaningful \n", })); let path = dir.path(); @@ -303,6 +318,7 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(js_lang()); language_registry.add(json_lang()); + language_registry.add(markdown_lang()); language_registry.add(rust_lang()); let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap()); @@ -325,11 +341,14 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { let settings_d = settings_for("d/d.rs", cx).await; let settings_e = settings_for("e/e.rs", cx).await; let settings_readme = settings_for("README.json", cx).await; + let settings_markdown = settings_for("README.md", cx).await; // .editorconfig overrides .zed/settings assert_eq!(Some(settings_a.tab_size), NonZeroU32::new(3)); assert_eq!(settings_a.hard_tabs, true); assert_eq!(settings_a.ensure_final_newline_on_save, true); - assert_eq!(settings_a.remove_trailing_whitespace_on_save, true); + // .editorconfig can disable trailing whitespace removal, but should not + // re-enable it when Zed settings have disabled it. + assert_eq!(settings_a.remove_trailing_whitespace_on_save, false); assert_eq!(settings_a.line_ending, LineEndingSetting::EnforceLf); assert_eq!(settings_a.preferred_line_length, 120); @@ -348,6 +367,9 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { // "indent_size" is not set, so "tab_width" is used assert_eq!(Some(settings_c.tab_size), NonZeroU32::new(10)); + assert_eq!(settings_c.remove_trailing_whitespace_on_save, false); + assert_eq!(settings_readme.remove_trailing_whitespace_on_save, true); + assert_eq!(settings_markdown.remove_trailing_whitespace_on_save, true); // When max_line_length is "off", default to .zed/settings.json assert_eq!(settings_b.preferred_line_length, 64);