Fix ESLint linebreak-style errors by preserving line endings in LSP communication (#38773)

Closes https://github.com/zed-industries/zed/issues/38453

Current `Buffer` API only allows getting buffer text with `\n` line
breaks — even if the `\r\n` was used in the original file's text.

This it not correct in certain cases like LSP formatting, where language
servers need to have original document context for e.g. formatting
purposes.

Added new `Buffer` API, replaced all buffer LSP registration places with
the new one and added more tests.

Release Notes: 

- Fixed ESLint linebreak-style errors by preserving line endings in LSP
communication

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
Devdatta Talele
2025-10-23 19:11:48 +00:00
committed by GitHub
co-authored by Claude Kirill Bulatov
parent 55bc679c19
commit 435eab6896
7 changed files with 273 additions and 91 deletions
+86 -3
View File
@@ -12629,6 +12629,12 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
);
}
});
#[cfg(target_os = "windows")]
let line_ending = "\r\n";
#[cfg(not(target_os = "windows"))]
let line_ending = "\n";
// Handle formatting requests to the language server.
cx.lsp
.set_request_handler::<lsp::request::Formatting, _, _>({
@@ -12652,7 +12658,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
),
(
lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
"\n".into()
line_ending.into()
),
]
);
@@ -12663,14 +12669,14 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
lsp::Position::new(1, 0),
lsp::Position::new(1, 0),
),
new_text: "\n".into(),
new_text: line_ending.into(),
},
lsp::TextEdit {
range: lsp::Range::new(
lsp::Position::new(2, 0),
lsp::Position::new(2, 0),
),
new_text: "\n".into(),
new_text: line_ending.into(),
},
]))
}
@@ -26656,6 +26662,83 @@ async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_mult
));
}
#[gpui::test]
async fn test_non_linux_line_endings_registration(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let unix_newlines_file_text = "fn main() {
let a = 5;
}";
let clrf_file_text = unix_newlines_file_text.lines().join("\r\n");
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/a"),
json!({
"first.rs": &clrf_file_text,
}),
)
.await;
let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let registered_text = Arc::new(Mutex::new(Vec::new()));
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
..lsp::ServerCapabilities::default()
},
name: "rust-analyzer",
initializer: Some({
let registered_text = registered_text.clone();
Box::new(move |fake_server| {
fake_server.handle_notification::<lsp::notification::DidOpenTextDocument, _>({
let registered_text = registered_text.clone();
move |params, _| {
registered_text.lock().push(params.text_document.text);
}
});
})
}),
..FakeLspAdapter::default()
},
);
let editor = workspace
.update(cx, |workspace, window, cx| {
workspace.open_abs_path(
PathBuf::from(path!("/a/first.rs")),
OpenOptions::default(),
window,
cx,
)
})
.unwrap()
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
let _fake_language_server = fake_servers.next().await.unwrap();
cx.executor().run_until_parked();
assert_eq!(
editor.update(cx, |editor, cx| editor.text(cx)),
unix_newlines_file_text,
"Default text API returns \n-separated text",
);
assert_eq!(
vec![clrf_file_text],
registered_text.lock().drain(..).collect::<Vec<_>>(),
"Expected the language server to receive the exact same text from the FS",
);
}
#[gpui::test]
async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
init_test(cx, |_| {});