diff --git a/assets/settings/default.json b/assets/settings/default.json index 2b899283841b8b580f6af009d8d1ec9c1ca24940..6cbe95849afb65c57bdef955cf0737cab60db49c 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1360,6 +1360,24 @@ // Removes any lines containing only whitespace at the end of the file and // ensures just one newline at the end. "ensure_final_newline_on_save": true, + // How line endings should be handled for new files and during format and save. + // This setting can take five values: + // + // 1. Detect existing line endings and otherwise use the platform default + // (`lf` on Unix, `crlf` on Windows): + // "line_ending": "detect" + // 2. Prefer LF (`\n`) for new files and files with no existing line ending: + // "line_ending": "prefer_lf" + // 3. Prefer CRLF (`\r\n`) for new files and files with no existing line ending: + // "line_ending": "prefer_crlf" + // 4. Enforce LF (`\n`) during format and save: + // "line_ending": "enforce_lf" + // 5. Enforce CRLF (`\r\n`) during format and save: + // "line_ending": "enforce_crlf" + // + // The EditorConfig `end_of_line` property overrides this setting and behaves + // like `enforce_lf` or `enforce_crlf`. + "line_ending": "detect", // Whether or not to perform a buffer format before saving: [on, off] // Keep in mind, if the autosave with delay is enabled, format_on_save will be ignored "format_on_save": "on", diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 986654e6fcd455ad2aa64bbd0a5548eeedd4afdd..adc874b666cc6c86faaeba4c99f709f8caad19dc 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -6,7 +6,9 @@ use crate::{ use collections::{FxHashMap, HashMap, HashSet}; use ec4rs::{ Properties as EditorconfigProperties, - property::{FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs}, + property::{ + EndOfLine, FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs, + }, }; use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; use gpui::{App, Modifiers, SharedString}; @@ -16,8 +18,8 @@ use settings::{DocumentFoldingRanges, DocumentSymbols, IntoGpui, SemanticTokens} pub use settings::{ AutoIndentMode, CompletionSettingsContent, EditPredictionPromptFormat, EditPredictionProvider, EditPredictionsMode, FormatOnSave, Formatter, FormatterList, InlayHintKind, - LanguageSettingsContent, LspInsertMode, RewrapBehavior, ShowWhitespaceSetting, SoftWrap, - WordsCompletionMode, + LanguageSettingsContent, LineEndingSetting, LspInsertMode, RewrapBehavior, + ShowWhitespaceSetting, SoftWrap, WordsCompletionMode, }; use settings::{RegisterSetting, Settings, SettingsLocation, SettingsStore, merge_from::MergeFrom}; use shellexpand; @@ -82,6 +84,9 @@ pub struct LanguageSettings { /// Whether or not to ensure there's a single newline at the end of a buffer /// when saving it. pub ensure_final_newline_on_save: bool, + /// How line endings are initialized for new files and normalized during + /// format and save. + pub line_ending: LineEndingSetting, /// How to perform a buffer format. pub formatter: settings::FormatterList, /// Zed's Prettier integration settings. @@ -637,6 +642,11 @@ fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigPr TrimTrailingWs::Value(b) => b, }) .ok(); + let line_ending = cfg.get::().ok().and_then(|v| match v { + EndOfLine::Lf => Some(LineEndingSetting::EnforceLf), + EndOfLine::CrLf => Some(LineEndingSetting::EnforceCrlf), + EndOfLine::Cr => None, + }); settings .preferred_line_length @@ -649,6 +659,7 @@ fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigPr settings .ensure_final_newline_on_save .merge_from_option(ensure_final_newline_on_save.as_ref()); + settings.line_ending.merge_from_option(line_ending.as_ref()); } impl settings::Settings for AllLanguageSettings { @@ -682,6 +693,7 @@ impl settings::Settings for AllLanguageSettings { .remove_trailing_whitespace_on_save .unwrap(), ensure_final_newline_on_save: settings.ensure_final_newline_on_save.unwrap(), + line_ending: settings.line_ending.unwrap(), formatter: settings.formatter.unwrap(), prettier: PrettierSettings { allowed: prettier.allowed.unwrap(), diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index d2f05a119a1883a1ec744b40d4cdb467074d3c83..b5828d60689d6a16666b7266eb131574b046645c 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -11,7 +11,8 @@ use gpui::{ App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, }; use language::{ - Buffer, BufferEvent, Capability, DiskState, File as _, Language, Operation, + Buffer, BufferEvent, Capability, DiskState, File as _, Language, LineEnding, Operation, + language_settings::{AllLanguageSettings, LineEndingSetting}, proto::{ deserialize_line_ending, deserialize_version, serialize_line_ending, serialize_version, split_operations, @@ -663,7 +664,7 @@ impl LocalBufferStore { Err(error) if is_not_found_error(&error) => cx.new(|cx| { let buffer_id = BufferId::from(cx.entity_id().as_non_zero_u64()); let text_buffer = text::Buffer::new(ReplicaId::LOCAL, buffer_id, ""); - Buffer::build( + let mut buffer = Buffer::build( text_buffer, Some(Arc::new(File { worktree, @@ -674,7 +675,9 @@ impl LocalBufferStore { is_private: false, })), Capability::ReadWrite, - ) + ); + apply_initial_line_ending(&mut buffer, cx); + buffer }), Err(e) => return Err(e), }; @@ -724,8 +727,10 @@ impl LocalBufferStore { ) -> Task>> { cx.spawn(async move |buffer_store, cx| { let buffer = cx.new(|cx| { - Buffer::local("", cx) - .with_language(language.unwrap_or_else(|| language::PLAIN_TEXT.clone()), cx) + let mut buffer = Buffer::local("", cx) + .with_language(language.unwrap_or_else(|| language::PLAIN_TEXT.clone()), cx); + apply_initial_line_ending(&mut buffer, cx); + buffer }); buffer_store.update(cx, |buffer_store, cx| { buffer_store.add_buffer(buffer.clone(), cx).log_err(); @@ -1628,8 +1633,10 @@ impl BufferStore { cx: &mut Context, ) -> Entity { let buffer = cx.new(|cx| { - Buffer::local(text, cx) - .with_language(language.unwrap_or_else(|| language::PLAIN_TEXT.clone()), cx) + let mut buffer = Buffer::local(text, cx) + .with_language(language.unwrap_or_else(|| language::PLAIN_TEXT.clone()), cx); + apply_initial_line_ending(&mut buffer, cx); + buffer }); self.add_buffer(buffer.clone(), cx).log_err(); @@ -1799,3 +1806,24 @@ fn is_not_found_error(error: &anyhow::Error) -> bool { .downcast_ref::() .is_some_and(|err| err.kind() == io::ErrorKind::NotFound) } + +fn apply_initial_line_ending(buffer: &mut Buffer, cx: &mut Context) { + // Only applies for empty rope or a single line with no trailing newline. + if buffer.max_point().row > 0 { + return; + } + let location = buffer.file().map(|file| settings::SettingsLocation { + worktree_id: file.worktree_id(cx), + path: file.path().as_ref(), + }); + let language = buffer.language().map(|l| l.name()); + let settings = AllLanguageSettings::get(location, cx).language(location, language.as_ref(), cx); + let desired = match settings.line_ending { + LineEndingSetting::Detect => return, + LineEndingSetting::PreferLf | LineEndingSetting::EnforceLf => LineEnding::Unix, + LineEndingSetting::PreferCrlf | LineEndingSetting::EnforceCrlf => LineEnding::Windows, + }; + if buffer.line_ending() != desired { + buffer.set_line_ending(desired, cx); + } +} diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 68e6265a7129f790918b1fea9251f9d709f1b5d7..5eb8eaf358b5ad91d43f4fa3e2ead0e458c66550 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -77,7 +77,8 @@ use language::{ OffsetUtf16, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToOffsetUtf16, ToPointUtf16, Toolchain, Transaction, Unclipped, language_settings::{ - AllLanguageSettings, FormatOnSave, Formatter, LanguageSettings, all_language_settings, + AllLanguageSettings, FormatOnSave, Formatter, LanguageSettings, LineEndingSetting, + all_language_settings, }, modeline, point_to_lsp, proto::{ @@ -1602,6 +1603,9 @@ impl LocalLspStore { (adapters_and_servers, settings, request_timeout) }) })?; + let had_existing_line_endings = buffer + .handle + .read_with(cx, |buffer, _| buffer.max_point().row > 0); // handle whitespace formatting if settings.remove_trailing_whitespace_on_save { @@ -1622,6 +1626,30 @@ impl LocalLspStore { })?; } + let line_ending_policy = match settings.line_ending { + LineEndingSetting::Detect => None, + LineEndingSetting::PreferLf => Some((LineEnding::Unix, true)), + LineEndingSetting::PreferCrlf => Some((LineEnding::Windows, true)), + LineEndingSetting::EnforceLf => Some((LineEnding::Unix, false)), + LineEndingSetting::EnforceCrlf => Some((LineEnding::Windows, false)), + }; + if let Some((desired_line_ending, preserve_existing)) = line_ending_policy { + buffer.handle.update(cx, |buffer, cx| { + if buffer.line_ending() == desired_line_ending { + return; + } + if preserve_existing && had_existing_line_endings { + zlog::trace!( + logger => "preserving existing line endings ({}) on save", + buffer.line_ending().label() + ); + return; + } + zlog::trace!(logger => "normalizing line endings to {}", desired_line_ending.label()); + buffer.set_line_ending(desired_line_ending, cx); + }); + } + // Formatter for `code_actions_on_format` that runs before // the rest of the formatters let mut code_actions_on_format_formatters = None; diff --git a/crates/project/tests/integration/project_tests.rs b/crates/project/tests/integration/project_tests.rs index bad9fcf58dc9392fd92f0c0930aa50fbe3b728d0..05e29d00177ec77daaa1df813e8883634f8a5cfd 100644 --- a/crates/project/tests/integration/project_tests.rs +++ b/crates/project/tests/integration/project_tests.rs @@ -46,7 +46,9 @@ use language::{ LanguageConfig, LanguageMatcher, LanguageName, LineEnding, ManifestName, ManifestProvider, ManifestQuery, OffsetRangeExt, Point, ToPoint, Toolchain, ToolchainList, ToolchainLister, ToolchainMetadata, - language_settings::{Formatter, FormatterList, LanguageSettings, LanguageSettingsContent}, + language_settings::{ + Formatter, FormatterList, LanguageSettings, LanguageSettingsContent, LineEndingSetting, + }, markdown_lang, rust_lang, tree_sitter_typescript, }; use lsp::{ @@ -318,6 +320,7 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { 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); + assert_eq!(settings_a.line_ending, LineEndingSetting::EnforceLf); assert_eq!(settings_a.preferred_line_length, 120); // .editorconfig in b/ overrides .editorconfig in root @@ -6420,6 +6423,289 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_line_ending_user_settings_on_format(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let cases = [ + ( + "default", + None, + [ + ("crlf_file.rs", LineEnding::Windows), + ("lf_file.rs", LineEnding::Unix), + ("no_newline.rs", LineEnding::default()), + ], + ), + ( + "detect", + Some(LineEndingSetting::Detect), + [ + ("crlf_file.rs", LineEnding::Windows), + ("lf_file.rs", LineEnding::Unix), + ("no_newline.rs", LineEnding::default()), + ], + ), + ( + "prefer_lf", + Some(LineEndingSetting::PreferLf), + [ + ("crlf_file.rs", LineEnding::Windows), + ("lf_file.rs", LineEnding::Unix), + ("no_newline.rs", LineEnding::Unix), + ], + ), + ( + "prefer_crlf", + Some(LineEndingSetting::PreferCrlf), + [ + ("crlf_file.rs", LineEnding::Windows), + ("lf_file.rs", LineEnding::Unix), + ("no_newline.rs", LineEnding::Windows), + ], + ), + ( + "enforce_lf", + Some(LineEndingSetting::EnforceLf), + [ + ("crlf_file.rs", LineEnding::Unix), + ("lf_file.rs", LineEnding::Unix), + ("no_newline.rs", LineEnding::Unix), + ], + ), + ( + "enforce_crlf", + Some(LineEndingSetting::EnforceCrlf), + [ + ("crlf_file.rs", LineEnding::Windows), + ("lf_file.rs", LineEnding::Windows), + ("no_newline.rs", LineEnding::Windows), + ], + ), + ]; + + for (case_name, line_ending_setting, expected_line_endings) in cases { + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/dir"), + json!({ + "crlf_file.rs": "one\r\ntwo\r\nthree\r\n", + "lf_file.rs": "one\ntwo\nthree\n", + "no_newline.rs": "single line", + }), + ) + .await; + + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + let worktree_id = project.update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }); + + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings(cx, |settings| { + settings.project.all_languages.defaults.line_ending = line_ending_setting; + }); + }); + }); + cx.executor().run_until_parked(); + + assert_line_endings_after_format( + cx, + &project, + worktree_id, + case_name, + &expected_line_endings, + ) + .await; + } +} + +#[gpui::test] +async fn test_line_ending_editorconfig_on_format_and_save(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let cases = [ + ( + "editorconfig lf", + "lf", + "crlf_file.rs", + LineEnding::Windows, + [ + ("crlf_file.rs", LineEnding::Unix), + ("lf_file.rs", LineEnding::Unix), + ("no_newline.rs", LineEnding::Unix), + ], + "one\ntwo\nthree\n", + ), + ( + "editorconfig crlf", + "crlf", + "lf_file.rs", + LineEnding::Unix, + [ + ("crlf_file.rs", LineEnding::Windows), + ("lf_file.rs", LineEnding::Windows), + ("no_newline.rs", LineEnding::Windows), + ], + "one\r\ntwo\r\nthree\r\n", + ), + ]; + + for ( + case_name, + editorconfig_end_of_line, + buffer_path, + initial_line_ending, + expected_line_endings, + expected_saved_contents, + ) in cases + { + let file_system = FakeFs::new(cx.executor()); + file_system + .insert_tree( + path!("/dir"), + json!({ + ".editorconfig": format!("root = true\n[*.rs]\nend_of_line = {editorconfig_end_of_line}\n"), + "crlf_file.rs": "one\r\ntwo\r\nthree\r\n", + "lf_file.rs": "one\ntwo\nthree\n", + "no_newline.rs": "single line", + }), + ) + .await; + + let project = Project::test(file_system.clone(), [path!("/dir").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + cx.executor().run_until_parked(); + let worktree_id = project.update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }); + + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, rel_path(buffer_path)), cx) + }) + .await + .unwrap(); + buffer.update(cx, |buffer, _| { + assert_eq!(buffer.line_ending(), initial_line_ending); + }); + + assert_line_endings_after_format( + cx, + &project, + worktree_id, + case_name, + &expected_line_endings, + ) + .await; + + project + .update(cx, |project, cx| project.save_buffer(buffer, cx)) + .await + .unwrap(); + let saved_path = PathBuf::from(path!("/dir")).join(buffer_path); + assert_eq!( + file_system.load(&saved_path).await.unwrap(), + expected_saved_contents, + ); + } +} + +#[gpui::test] +async fn test_line_ending_initialization_for_new_buffers(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let cases = [ + (Some(LineEndingSetting::Detect), LineEnding::default()), + (Some(LineEndingSetting::PreferLf), LineEnding::Unix), + (Some(LineEndingSetting::PreferCrlf), LineEnding::Windows), + (Some(LineEndingSetting::EnforceLf), LineEnding::Unix), + (Some(LineEndingSetting::EnforceCrlf), LineEnding::Windows), + ]; + + for (line_ending_setting, expected_line_ending) in cases { + let fs = FakeFs::new(cx.executor()); + fs.insert_tree(path!("/dir"), json!({})).await; + + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings(cx, |settings| { + settings.project.all_languages.defaults.line_ending = line_ending_setting; + }); + }); + }); + cx.executor().run_until_parked(); + + let created_buffer = project + .update(cx, |project, cx| project.create_buffer(None, false, cx)) + .unwrap() + .await; + created_buffer.update(cx, |buffer, _| { + assert_eq!(buffer.line_ending(), expected_line_ending); + }); + + let local_buffer = project.update(cx, |project, cx| { + project.create_local_buffer("single line", None, false, cx) + }); + local_buffer.update(cx, |buffer, _| { + assert_eq!(buffer.line_ending(), expected_line_ending); + }); + + let opened_missing_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer(path!("/dir/new_file.rs"), cx) + }) + .await + .unwrap(); + opened_missing_buffer.update(cx, |buffer, _| { + assert_eq!(buffer.line_ending(), expected_line_ending); + }); + } +} + +async fn assert_line_endings_after_format( + cx: &mut gpui::TestAppContext, + project: &Entity, + worktree_id: WorktreeId, + case_name: &str, + expected_line_endings: &[(&str, LineEnding)], +) { + for (path, expected_line_ending) in expected_line_endings { + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, rel_path(path)), cx) + }) + .await + .unwrap(); + let mut buffers = HashSet::default(); + buffers.insert(buffer.clone()); + project + .update(cx, |project, cx| { + project.format( + buffers, + project::lsp_store::LspFormatTarget::Buffers, + false, + project::lsp_store::FormatTrigger::Save, + cx, + ) + }) + .await + .unwrap(); + buffer.update(cx, |buffer, _| { + assert_eq!( + buffer.line_ending(), + *expected_line_ending, + "unexpected line ending for {path} in {case_name}" + ); + }); + } +} + #[gpui::test] async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { init_test(cx); diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index 2fdd8e645803ba06354082bd01c9b0c6916f33fa..323b8d7fef0c4352dab594f8b235e3c518328565 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -540,6 +540,12 @@ impl VsCodeSettings { edit_predictions_disabled_in: None, enable_language_server: None, ensure_final_newline_on_save: self.read_bool("files.insertFinalNewline"), + line_ending: self.read_enum("files.eol", |s| match s { + "\n" => Some(LineEndingSetting::PreferLf), + "\r\n" => Some(LineEndingSetting::PreferCrlf), + "auto" => Some(LineEndingSetting::Detect), + _ => None, + }), extend_comment_on_newline: None, extend_list_on_newline: None, indent_list_on_tab: None, diff --git a/crates/settings_content/src/language.rs b/crates/settings_content/src/language.rs index c4a674822a81ed096c58143ea560cb116aa54646..56dbb141316f3debad36ef8cb773be78c4b0b8c1 100644 --- a/crates/settings_content/src/language.rs +++ b/crates/settings_content/src/language.rs @@ -449,6 +449,23 @@ pub struct LanguageSettingsContent { /// /// Default: true pub ensure_final_newline_on_save: Option, + /// How line endings should be handled for new files and during format and + /// save operations. + /// + /// - `detect`: Detect existing line endings and otherwise use the platform + /// default (`lf` on Unix, `crlf` on Windows). + /// - `prefer_lf`: Prefer LF for new files and files with no existing line + /// ending. + /// - `prefer_crlf`: Prefer CRLF for new files and files with no existing + /// line ending. + /// - `enforce_lf`: Enforce LF during format and save. + /// - `enforce_crlf`: Enforce CRLF during format and save. + /// + /// The EditorConfig `end_of_line` property overrides this setting and + /// behaves like `enforce_lf` or `enforce_crlf`. + /// + /// Default: detect + pub line_ending: Option, /// How to perform a buffer format. /// /// Default: auto @@ -899,6 +916,42 @@ pub enum FormatOnSave { Off, } +/// Controls how line endings are normalized when a buffer is saved. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize, + Deserialize, + JsonSchema, + MergeFrom, + strum::VariantArray, + strum::VariantNames, +)] +#[serde(rename_all = "snake_case")] +pub enum LineEndingSetting { + /// Preserve the existing line endings of the file. New files use the + /// platform default line ending. + #[strum(serialize = "Detect")] + Detect, + /// Use LF for new files and files with no existing line-ending + /// convention, while preserving existing LF or CRLF files. + #[strum(serialize = "Prefer LF")] + PreferLf, + /// Use CRLF for new files and files with no existing line-ending + /// convention, while preserving existing LF or CRLF files. + #[strum(serialize = "Prefer CRLF")] + PreferCrlf, + /// Normalize line endings to LF (`\n`) during format and save. + #[strum(serialize = "Enforce LF")] + EnforceLf, + /// Normalize line endings to CRLF (`\r\n`) during format and save. + #[strum(serialize = "Enforce CRLF")] + EnforceCrlf, +} + /// Controls which formatters should be used when formatting code. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] #[serde(untagged)] diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 52557256f1431a5f897c400ea339b73e9af319c8..d0b0120955aa82079c5617db50bfce96c0e03f9a 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -8083,7 +8083,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { ] } - fn formatting_section() -> [SettingsPageItem; 7] { + fn formatting_section() -> [SettingsPageItem; 8] { [ SettingsPageItem::SectionHeader("Formatting"), SettingsPageItem::SettingItem(SettingItem { @@ -8150,6 +8150,28 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { metadata: None, files: USER | PROJECT, }), + SettingsPageItem::SettingItem(SettingItem { + title: "Line Ending", + description: "How line endings should be handled for new files and during format and save operations.", + field: Box::new(SettingField { + json_path: Some("languages.$(language).line_ending"), + pick: |settings_content| { + language_settings_field(settings_content, |language| { + language.line_ending.as_ref() + }) + }, + write: |settings_content, value| { + language_settings_field_mut(settings_content, value, |language, value| { + language.line_ending = value; + }) + }, + }), + metadata: Some(Box::new(SettingsFieldMetadata { + should_do_titlecase: Some(false), + ..Default::default() + })), + files: USER | PROJECT, + }), SettingsPageItem::SettingItem(SettingItem { title: "Formatter", description: "How to perform a buffer format.", diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 1567a87445a184dd18d2e1d1a0ed4c31c30f1415..54d71f1b53621939c4c53c057f9d50c2c1ccb294 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -491,6 +491,7 @@ fn init_renderers(cx: &mut App) { .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) + .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) diff --git a/docs/src/reference/all-settings.md b/docs/src/reference/all-settings.md index c0da7734f4e7d448170805c601c7b9573f3b7a73..4e905c2f3ac9d735bdc2b3875cc7eb78b8f4562e 100644 --- a/docs/src/reference/all-settings.md +++ b/docs/src/reference/all-settings.md @@ -1618,6 +1618,56 @@ This setting enables integration with macOS’s native window tabbing feature. W `boolean` values +## Line Ending + +- Description: How line endings should be handled for new files and during format and save. This can be specified on a per-language basis. +- Setting: `line_ending` +- Default: `detect` + +**Options** + +1. To detect existing line endings and otherwise use the platform default (`lf` on Unix, `crlf` on Windows), set it to `detect`: + +```json [settings] +{ + "line_ending": "detect" +} +``` + +2. To prefer LF (`\n`) for new files and files with no existing line ending, use `prefer_lf`: + +```json [settings] +{ + "line_ending": "prefer_lf" +} +``` + +3. To prefer CRLF (`\r\n`) for new files and files with no existing line ending, use `prefer_crlf`: + +```json [settings] +{ + "line_ending": "prefer_crlf" +} +``` + +4. To enforce LF (`\n`) during format and save, use `enforce_lf`: + +```json [settings] +{ + "line_ending": "enforce_lf" +} +``` + +5. To enforce CRLF (`\r\n`) during format and save, use `enforce_crlf`: + +```json [settings] +{ + "line_ending": "enforce_crlf" +} +``` + +The [`.editorconfig`](https://editorconfig.org) `end_of_line` property overrides this setting and behaves like `enforce_lf` or `enforce_crlf`. + ## Expand Excerpt Lines - Description: The default number of lines to expand excerpts in the multibuffer by @@ -2772,6 +2822,7 @@ The following settings can be overridden for each specific language: - [`enable_language_server`](#enable-language-server) - [`ensure_final_newline_on_save`](#ensure-final-newline-on-save) +- [`line_ending`](#line-ending) - [`format_on_save`](#format-on-save) - [`formatter`](#formatter) - [`hard_tabs`](#hard-tabs)