Detailed changes
@@ -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",
@@ -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::<EndOfLine>().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(),
@@ -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<Result<Entity<Buffer>>> {
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<Self>,
) -> Entity<Buffer> {
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::<io::Error>()
.is_some_and(|err| err.kind() == io::ErrorKind::NotFound)
}
+
+fn apply_initial_line_ending(buffer: &mut Buffer, cx: &mut Context<Buffer>) {
+ // 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);
+ }
+}
@@ -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;
@@ -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<Project>,
+ 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);
@@ -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,
@@ -449,6 +449,23 @@ pub struct LanguageSettingsContent {
///
/// Default: true
pub ensure_final_newline_on_save: Option<bool>,
+ /// 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<LineEndingSetting>,
/// 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)]
@@ -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.",
@@ -491,6 +491,7 @@ fn init_renderers(cx: &mut App) {
.add_basic_renderer::<settings::ProjectPanelSortOrder>(render_dropdown)
.add_basic_renderer::<settings::RewrapBehavior>(render_dropdown)
.add_basic_renderer::<settings::FormatOnSave>(render_dropdown)
+ .add_basic_renderer::<settings::LineEndingSetting>(render_dropdown)
.add_basic_renderer::<settings::IndentGuideColoring>(render_dropdown)
.add_basic_renderer::<settings::IndentGuideBackgroundColoring>(render_dropdown)
.add_basic_renderer::<settings::FileFinderWidthContent>(render_dropdown)
@@ -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)