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
+19 -77
View File
@@ -20,11 +20,9 @@ use operation_queue::OperationQueue;
pub use patch::Patch;
use postage::{oneshot, prelude::*};
use regex::Regex;
pub use rope::*;
pub use selection::*;
use std::{
borrow::Cow,
cmp::{self, Ordering, Reverse},
fmt::Display,
future::Future,
@@ -32,7 +30,7 @@ use std::{
num::NonZeroU64,
ops::{self, Deref, Range, Sub},
str,
sync::{Arc, LazyLock},
sync::Arc,
time::{Duration, Instant},
};
pub use subscription::*;
@@ -43,9 +41,6 @@ use undo_map::UndoMap;
#[cfg(any(test, feature = "test-support"))]
use util::RandomCharIter;
static LINE_SEPARATORS_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\r\n|\r").expect("Failed to create LINE_SEPARATORS_REGEX"));
pub type TransactionId = clock::Lamport;
pub struct Buffer {
@@ -2019,10 +2014,24 @@ impl BufferSnapshot {
start..position
}
/// Returns the buffer's text as a String.
///
/// Note: This always uses `\n` as the line separator, regardless of the buffer's
/// actual line ending setting. For LSP communication or other cases where you need
/// to preserve the original line endings, use [`Self::text_with_original_line_endings`] instead.
pub fn text(&self) -> String {
self.visible_text.to_string()
}
/// Returns the buffer's text with line same endings as in buffer's file.
///
/// Unlike [`Self::text`] which always uses `\n`, this method formats the text using
/// the buffer's actual line ending setting (Unix `\n` or Windows `\r\n`).
pub fn text_with_original_line_endings(&self) -> String {
self.visible_text
.to_string_with_line_ending(self.line_ending)
}
pub fn line_ending(&self) -> LineEnding {
self.line_ending
}
@@ -2126,6 +2135,10 @@ impl BufferSnapshot {
self.visible_text.reversed_bytes_in_range(start..end)
}
/// Returns the text in the given range.
///
/// Note: This always uses `\n` as the line separator, regardless of the buffer's
/// actual line ending setting.
pub fn text_for_range<T: ToOffset>(&self, range: Range<T>) -> Chunks<'_> {
let start = range.start.to_offset(self);
let end = range.end.to_offset(self);
@@ -3246,77 +3259,6 @@ impl FromAnchor for usize {
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum LineEnding {
Unix,
Windows,
}
impl Default for LineEnding {
fn default() -> Self {
#[cfg(unix)]
return Self::Unix;
#[cfg(not(unix))]
return Self::Windows;
}
}
impl LineEnding {
pub fn as_str(&self) -> &'static str {
match self {
LineEnding::Unix => "\n",
LineEnding::Windows => "\r\n",
}
}
pub fn label(&self) -> &'static str {
match self {
LineEnding::Unix => "LF",
LineEnding::Windows => "CRLF",
}
}
pub fn detect(text: &str) -> Self {
let mut max_ix = cmp::min(text.len(), 1000);
while !text.is_char_boundary(max_ix) {
max_ix -= 1;
}
if let Some(ix) = text[..max_ix].find(['\n']) {
if ix > 0 && text.as_bytes()[ix - 1] == b'\r' {
Self::Windows
} else {
Self::Unix
}
} else {
Self::default()
}
}
pub fn normalize(text: &mut String) {
if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(text, "\n") {
*text = replaced;
}
}
pub fn normalize_arc(text: Arc<str>) -> Arc<str> {
if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") {
replaced.into()
} else {
text
}
}
pub fn normalize_cow(text: Cow<str>) -> Cow<str> {
if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") {
replaced.into()
} else {
text
}
}
}
#[cfg(debug_assertions)]
pub mod debug {
use super::*;