Add line_ending setting to control line-ending normalization (#54356)
Closes #49581 Adds a `line_ending` language setting that controls how line endings are handled for new files and during format/save: - `detect` (default) — detects existing line endings; new files use the platform default - `prefer_lf` / `prefer_crlf` — sets LF or CRLF for new files and files with no existing convention, while preserving existing files - `enforce_lf` / `enforce_crlf` — normalizes all line endings to LF or CRLF on every format/save The setting can be configured globally, per-language, or via `.editorconfig`'s `end_of_line` property (which maps to `enforce_lf` / `enforce_crlf`). Release Notes: - Added `line_ending` setting to control how line endings are handled for new files and normalized on save. - Added support for `.editorconfig` `end_of_line` property to enforce line endings. --------- Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
1 parent
c97442029a
commit
2a6ee04013
10 files changed
+518
-13
No files matched your search
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user