Merge pull request #1287 from zed-industries/handle-carriage-returns-in-edits

Max Brunsfeld created

Fix crash when pasting text with CRLF line endings

Change summary

crates/language/src/buffer.rs       | 104 +++++-------------------------
crates/language/src/proto.rs        |  14 ++++
crates/language/src/tests.rs        |   2 
crates/project/src/project.rs       |  12 ++-
crates/project/src/worktree.rs      |   4 
crates/text/src/random_char_iter.rs |   2 
crates/text/src/rope.rs             |   9 --
crates/text/src/tests.rs            |  52 ++++++++++-----
crates/text/src/text.rs             |  82 ++++++++++++++++++++++--
9 files changed, 155 insertions(+), 126 deletions(-)

Detailed changes

crates/language/src/buffer.rs 🔗

@@ -53,7 +53,6 @@ pub struct Buffer {
     saved_version: clock::Global,
     saved_version_fingerprint: String,
     saved_mtime: SystemTime,
-    line_ending: LineEnding,
     transaction_depth: usize,
     was_dirty_before_starting_transaction: Option<bool>,
     language: Option<Arc<Language>>,
@@ -98,12 +97,6 @@ pub enum IndentKind {
     Tab,
 }
 
-#[derive(Copy, Debug, Clone, PartialEq, Eq)]
-pub enum LineEnding {
-    Unix,
-    Windows,
-}
-
 #[derive(Clone, Debug)]
 struct SelectionSet {
     line_mode: bool,
@@ -314,32 +307,26 @@ impl CharKind {
 }
 
 impl Buffer {
-    pub fn new<T: Into<Arc<str>>>(
+    pub fn new<T: Into<String>>(
         replica_id: ReplicaId,
         base_text: T,
         cx: &mut ModelContext<Self>,
     ) -> Self {
-        let history = History::new(base_text.into());
-        let line_ending = LineEnding::detect(&history.base_text);
         Self::build(
-            TextBuffer::new(replica_id, cx.model_id() as u64, history),
+            TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()),
             None,
-            line_ending,
         )
     }
 
-    pub fn from_file<T: Into<Arc<str>>>(
+    pub fn from_file<T: Into<String>>(
         replica_id: ReplicaId,
         base_text: T,
         file: Arc<dyn File>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
-        let history = History::new(base_text.into());
-        let line_ending = LineEnding::detect(&history.base_text);
         Self::build(
-            TextBuffer::new(replica_id, cx.model_id() as u64, history),
+            TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()),
             Some(file),
-            line_ending,
         )
     }
 
@@ -349,14 +336,12 @@ impl Buffer {
         file: Option<Arc<dyn File>>,
         cx: &mut ModelContext<Self>,
     ) -> Result<Self> {
-        let buffer = TextBuffer::new(
-            replica_id,
-            message.id,
-            History::new(Arc::from(message.base_text)),
-        );
-        let line_ending = proto::LineEnding::from_i32(message.line_ending)
-            .ok_or_else(|| anyhow!("missing line_ending"))?;
-        let mut this = Self::build(buffer, file, LineEnding::from_proto(line_ending));
+        let buffer = TextBuffer::new(replica_id, message.id, message.base_text);
+        let mut this = Self::build(buffer, file);
+        this.text.set_line_ending(proto::deserialize_line_ending(
+            proto::LineEnding::from_i32(message.line_ending)
+                .ok_or_else(|| anyhow!("missing line_ending"))?,
+        ));
         let ops = message
             .operations
             .into_iter()
@@ -421,7 +406,7 @@ impl Buffer {
             diagnostics: proto::serialize_diagnostics(self.diagnostics.iter()),
             diagnostics_timestamp: self.diagnostics_timestamp.value,
             completion_triggers: self.completion_triggers.clone(),
-            line_ending: self.line_ending.to_proto() as i32,
+            line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
         }
     }
 
@@ -430,7 +415,7 @@ impl Buffer {
         self
     }
 
-    fn build(buffer: TextBuffer, file: Option<Arc<dyn File>>, line_ending: LineEnding) -> Self {
+    fn build(buffer: TextBuffer, file: Option<Arc<dyn File>>) -> Self {
         let saved_mtime;
         if let Some(file) = file.as_ref() {
             saved_mtime = file.mtime();
@@ -446,7 +431,6 @@ impl Buffer {
             was_dirty_before_starting_transaction: None,
             text: buffer,
             file,
-            line_ending,
             syntax_tree: Mutex::new(None),
             parsing_in_background: false,
             parse_count: 0,
@@ -507,7 +491,7 @@ impl Buffer {
             self.remote_id(),
             text,
             version,
-            self.line_ending,
+            self.line_ending(),
             cx.as_mut(),
         );
         cx.spawn(|this, mut cx| async move {
@@ -563,7 +547,7 @@ impl Buffer {
                         this.did_reload(
                             this.version(),
                             this.as_rope().fingerprint(),
-                            this.line_ending,
+                            this.line_ending(),
                             new_mtime,
                             cx,
                         );
@@ -588,14 +572,14 @@ impl Buffer {
     ) {
         self.saved_version = version;
         self.saved_version_fingerprint = fingerprint;
-        self.line_ending = line_ending;
+        self.text.set_line_ending(line_ending);
         self.saved_mtime = mtime;
         if let Some(file) = self.file.as_ref().and_then(|f| f.as_local()) {
             file.buffer_reloaded(
                 self.remote_id(),
                 &self.saved_version,
                 self.saved_version_fingerprint.clone(),
-                self.line_ending,
+                self.line_ending(),
                 self.saved_mtime,
                 cx,
             );
@@ -974,13 +958,13 @@ impl Buffer {
         }
     }
 
-    pub(crate) fn diff(&self, new_text: String, cx: &AppContext) -> Task<Diff> {
+    pub(crate) fn diff(&self, mut new_text: String, cx: &AppContext) -> Task<Diff> {
         let old_text = self.as_rope().clone();
         let base_version = self.version();
         cx.background().spawn(async move {
             let old_text = old_text.to_string();
             let line_ending = LineEnding::detect(&new_text);
-            let new_text = new_text.replace("\r\n", "\n").replace('\r', "\n");
+            LineEnding::strip_carriage_returns(&mut new_text);
             let changes = TextDiff::from_lines(old_text.as_str(), new_text.as_str())
                 .iter_all_changes()
                 .map(|c| (c.tag(), c.value().len()))
@@ -1003,7 +987,7 @@ impl Buffer {
         if self.version == diff.base_version {
             self.finalize_last_transaction();
             self.start_transaction();
-            self.line_ending = diff.line_ending;
+            self.text.set_line_ending(diff.line_ending);
             let mut offset = diff.start_offset;
             for (tag, len) in diff.changes {
                 let range = offset..(offset + len);
@@ -1518,10 +1502,6 @@ impl Buffer {
     pub fn completion_triggers(&self) -> &[String] {
         &self.completion_triggers
     }
-
-    pub fn line_ending(&self) -> LineEnding {
-        self.line_ending
-    }
 }
 
 #[cfg(any(test, feature = "test-support"))]
@@ -2542,52 +2522,6 @@ impl std::ops::SubAssign for IndentSize {
     }
 }
 
-impl LineEnding {
-    pub fn from_proto(style: proto::LineEnding) -> Self {
-        match style {
-            proto::LineEnding::Unix => Self::Unix,
-            proto::LineEnding::Windows => Self::Windows,
-        }
-    }
-
-    fn detect(text: &str) -> Self {
-        let text = &text[..cmp::min(text.len(), 1000)];
-        if let Some(ix) = text.find('\n') {
-            if ix == 0 || text.as_bytes()[ix - 1] != b'\r' {
-                Self::Unix
-            } else {
-                Self::Windows
-            }
-        } else {
-            Default::default()
-        }
-    }
-
-    pub fn as_str(self) -> &'static str {
-        match self {
-            LineEnding::Unix => "\n",
-            LineEnding::Windows => "\r\n",
-        }
-    }
-
-    pub fn to_proto(self) -> proto::LineEnding {
-        match self {
-            LineEnding::Unix => proto::LineEnding::Unix,
-            LineEnding::Windows => proto::LineEnding::Windows,
-        }
-    }
-}
-
-impl Default for LineEnding {
-    fn default() -> Self {
-        #[cfg(unix)]
-        return Self::Unix;
-
-        #[cfg(not(unix))]
-        return Self::Windows;
-    }
-}
-
 impl Completion {
     pub fn sort_key(&self) -> (usize, &str) {
         let kind_key = match self.lsp_completion.kind {

crates/language/src/proto.rs 🔗

@@ -11,6 +11,20 @@ use text::*;
 
 pub use proto::{Buffer, BufferState, LineEnding, SelectionSet};
 
+pub fn deserialize_line_ending(message: proto::LineEnding) -> text::LineEnding {
+    match message {
+        LineEnding::Unix => text::LineEnding::Unix,
+        LineEnding::Windows => text::LineEnding::Windows,
+    }
+}
+
+pub fn serialize_line_ending(message: text::LineEnding) -> proto::LineEnding {
+    match message {
+        text::LineEnding::Unix => proto::LineEnding::Unix,
+        text::LineEnding::Windows => proto::LineEnding::Windows,
+    }
+}
+
 pub fn serialize_operation(operation: &Operation) -> proto::Operation {
     proto::Operation {
         variant: Some(match operation {

crates/language/src/tests.rs 🔗

@@ -421,7 +421,7 @@ async fn test_outline(cx: &mut gpui::TestAppContext) {
     async fn search<'a>(
         outline: &'a Outline<Anchor>,
         query: &str,
-        cx: &gpui::TestAppContext,
+        cx: &'a gpui::TestAppContext,
     ) -> Vec<(&'a str, Vec<usize>)> {
         let matches = cx
             .read(|cx| outline.search(query, cx.background().clone()))

crates/project/src/project.rs 🔗

@@ -20,12 +20,14 @@ use gpui::{
 };
 use language::{
     point_to_lsp,
-    proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
+    proto::{
+        deserialize_anchor, deserialize_line_ending, deserialize_version, serialize_anchor,
+        serialize_version,
+    },
     range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CharKind, CodeAction, CodeLabel,
     Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _,
-    Language, LanguageRegistry, LanguageServerName, LineEnding, LocalFile, LspAdapter,
-    OffsetRangeExt, Operation, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16,
-    Transaction,
+    Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapter, OffsetRangeExt,
+    Operation, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction,
 };
 use lsp::{
     DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer, LanguageString,
@@ -5542,7 +5544,7 @@ impl Project {
     ) -> Result<()> {
         let payload = envelope.payload;
         let version = deserialize_version(payload.version);
-        let line_ending = LineEnding::from_proto(
+        let line_ending = deserialize_line_ending(
             proto::LineEnding::from_i32(payload.line_ending)
                 .ok_or_else(|| anyhow!("missing line ending"))?,
         );

crates/project/src/worktree.rs 🔗

@@ -23,7 +23,7 @@ use gpui::{
     Task,
 };
 use language::{
-    proto::{deserialize_version, serialize_version},
+    proto::{deserialize_version, serialize_line_ending, serialize_version},
     Buffer, DiagnosticEntry, LineEnding, PointUtf16, Rope,
 };
 use lazy_static::lazy_static;
@@ -1750,7 +1750,7 @@ impl language::LocalFile for File {
                     version: serialize_version(&version),
                     mtime: Some(mtime.into()),
                     fingerprint,
-                    line_ending: line_ending.to_proto() as i32,
+                    line_ending: serialize_line_ending(line_ending) as i32,
                 })
                 .log_err();
         }

crates/text/src/random_char_iter.rs 🔗

@@ -22,7 +22,7 @@ impl<T: Rng> Iterator for RandomCharIter<T> {
 
         match self.0.gen_range(0..100) {
             // whitespace
-            0..=19 => [' ', '\n', '\t'].choose(&mut self.0).copied(),
+            0..=19 => [' ', '\n', '\r', '\t'].choose(&mut self.0).copied(),
             // two-byte greek letters
             20..=32 => char::from_u32(self.0.gen_range(('α' as u32)..('ω' as u32 + 1))),
             // // three-byte characters

crates/text/src/rope.rs 🔗

@@ -58,19 +58,12 @@ impl Rope {
     pub fn push(&mut self, text: &str) {
         let mut new_chunks = SmallVec::<[_; 16]>::new();
         let mut new_chunk = ArrayString::new();
-        let mut chars = text.chars().peekable();
-        while let Some(mut ch) = chars.next() {
+        for ch in text.chars() {
             if new_chunk.len() + ch.len_utf8() > 2 * CHUNK_BASE {
                 new_chunks.push(Chunk(new_chunk));
                 new_chunk = ArrayString::new();
             }
 
-            if ch == '\r' {
-                ch = '\n';
-                if chars.peek().copied() == Some('\n') {
-                    chars.next();
-                }
-            }
             new_chunk.push(ch);
         }
         if !new_chunk.is_empty() {

crates/text/src/tests.rs 🔗

@@ -18,7 +18,7 @@ fn init_logger() {
 
 #[test]
 fn test_edit() {
-    let mut buffer = Buffer::new(0, 0, History::new("abc".into()));
+    let mut buffer = Buffer::new(0, 0, "abc".into());
     assert_eq!(buffer.text(), "abc");
     buffer.edit([(3..3, "def")]);
     assert_eq!(buffer.text(), "abcdef");
@@ -42,7 +42,9 @@ fn test_random_edits(mut rng: StdRng) {
     let mut reference_string = RandomCharIter::new(&mut rng)
         .take(reference_string_len)
         .collect::<String>();
-    let mut buffer = Buffer::new(0, 0, History::new(reference_string.clone().into()));
+    let mut buffer = Buffer::new(0, 0, reference_string.clone().into());
+    reference_string = reference_string.replace("\r", "");
+
     buffer.history.group_interval = Duration::from_millis(rng.gen_range(0..=200));
     let mut buffer_versions = Vec::new();
     log::info!(
@@ -56,6 +58,8 @@ fn test_random_edits(mut rng: StdRng) {
         for (old_range, new_text) in edits.iter().rev() {
             reference_string.replace_range(old_range.clone(), &new_text);
         }
+        reference_string = reference_string.replace("\r", "");
+
         assert_eq!(buffer.text(), reference_string);
         log::info!(
             "buffer text {:?}, version: {:?}",
@@ -148,9 +152,23 @@ fn test_random_edits(mut rng: StdRng) {
     }
 }
 
+#[test]
+fn test_line_endings() {
+    let mut buffer = Buffer::new(0, 0, "one\r\ntwo".into());
+    assert_eq!(buffer.text(), "one\ntwo");
+    assert_eq!(buffer.line_ending(), LineEnding::Windows);
+    buffer.check_invariants();
+
+    buffer.edit([(buffer.len()..buffer.len(), "\r\nthree")]);
+    buffer.edit([(0..0, "zero\r\n")]);
+    assert_eq!(buffer.text(), "zero\none\ntwo\nthree");
+    assert_eq!(buffer.line_ending(), LineEnding::Windows);
+    buffer.check_invariants();
+}
+
 #[test]
 fn test_line_len() {
-    let mut buffer = Buffer::new(0, 0, History::new("".into()));
+    let mut buffer = Buffer::new(0, 0, "".into());
     buffer.edit([(0..0, "abcd\nefg\nhij")]);
     buffer.edit([(12..12, "kl\nmno")]);
     buffer.edit([(18..18, "\npqrs\n")]);
@@ -167,7 +185,7 @@ fn test_line_len() {
 #[test]
 fn test_common_prefix_at_positionn() {
     let text = "a = str; b = δα";
-    let buffer = Buffer::new(0, 0, History::new(text.into()));
+    let buffer = Buffer::new(0, 0, text.into());
 
     let offset1 = offset_after(text, "str");
     let offset2 = offset_after(text, "δα");
@@ -215,7 +233,7 @@ fn test_common_prefix_at_positionn() {
 
 #[test]
 fn test_text_summary_for_range() {
-    let buffer = Buffer::new(0, 0, History::new("ab\nefg\nhklm\nnopqrs\ntuvwxyz".into()));
+    let buffer = Buffer::new(0, 0, "ab\nefg\nhklm\nnopqrs\ntuvwxyz".into());
     assert_eq!(
         buffer.text_summary_for_range::<TextSummary, _>(1..3),
         TextSummary {
@@ -280,7 +298,7 @@ fn test_text_summary_for_range() {
 
 #[test]
 fn test_chars_at() {
-    let mut buffer = Buffer::new(0, 0, History::new("".into()));
+    let mut buffer = Buffer::new(0, 0, "".into());
     buffer.edit([(0..0, "abcd\nefgh\nij")]);
     buffer.edit([(12..12, "kl\nmno")]);
     buffer.edit([(18..18, "\npqrs")]);
@@ -302,7 +320,7 @@ fn test_chars_at() {
     assert_eq!(chars.collect::<String>(), "PQrs");
 
     // Regression test:
-    let mut buffer = Buffer::new(0, 0, History::new("".into()));
+    let mut buffer = Buffer::new(0, 0, "".into());
     buffer.edit([(0..0, "[workspace]\nmembers = [\n    \"xray_core\",\n    \"xray_server\",\n    \"xray_cli\",\n    \"xray_wasm\",\n]\n")]);
     buffer.edit([(60..60, "\n")]);
 
@@ -312,7 +330,7 @@ fn test_chars_at() {
 
 #[test]
 fn test_anchors() {
-    let mut buffer = Buffer::new(0, 0, History::new("".into()));
+    let mut buffer = Buffer::new(0, 0, "".into());
     buffer.edit([(0..0, "abc")]);
     let left_anchor = buffer.anchor_before(2);
     let right_anchor = buffer.anchor_after(2);
@@ -430,7 +448,7 @@ fn test_anchors() {
 
 #[test]
 fn test_anchors_at_start_and_end() {
-    let mut buffer = Buffer::new(0, 0, History::new("".into()));
+    let mut buffer = Buffer::new(0, 0, "".into());
     let before_start_anchor = buffer.anchor_before(0);
     let after_end_anchor = buffer.anchor_after(0);
 
@@ -453,7 +471,7 @@ fn test_anchors_at_start_and_end() {
 
 #[test]
 fn test_undo_redo() {
-    let mut buffer = Buffer::new(0, 0, History::new("1234".into()));
+    let mut buffer = Buffer::new(0, 0, "1234".into());
     // Set group interval to zero so as to not group edits in the undo stack.
     buffer.history.group_interval = Duration::from_secs(0);
 
@@ -490,7 +508,7 @@ fn test_undo_redo() {
 #[test]
 fn test_history() {
     let mut now = Instant::now();
-    let mut buffer = Buffer::new(0, 0, History::new("123456".into()));
+    let mut buffer = Buffer::new(0, 0, "123456".into());
 
     buffer.start_transaction_at(now);
     buffer.edit([(2..4, "cd")]);
@@ -544,7 +562,7 @@ fn test_history() {
 #[test]
 fn test_finalize_last_transaction() {
     let now = Instant::now();
-    let mut buffer = Buffer::new(0, 0, History::new("123456".into()));
+    let mut buffer = Buffer::new(0, 0, "123456".into());
 
     buffer.start_transaction_at(now);
     buffer.edit([(2..4, "cd")]);
@@ -579,7 +597,7 @@ fn test_finalize_last_transaction() {
 #[test]
 fn test_edited_ranges_for_transaction() {
     let now = Instant::now();
-    let mut buffer = Buffer::new(0, 0, History::new("1234567".into()));
+    let mut buffer = Buffer::new(0, 0, "1234567".into());
 
     buffer.start_transaction_at(now);
     buffer.edit([(2..4, "cd")]);
@@ -618,9 +636,9 @@ fn test_edited_ranges_for_transaction() {
 fn test_concurrent_edits() {
     let text = "abcdef";
 
-    let mut buffer1 = Buffer::new(1, 0, History::new(text.into()));
-    let mut buffer2 = Buffer::new(2, 0, History::new(text.into()));
-    let mut buffer3 = Buffer::new(3, 0, History::new(text.into()));
+    let mut buffer1 = Buffer::new(1, 0, text.into());
+    let mut buffer2 = Buffer::new(2, 0, text.into());
+    let mut buffer3 = Buffer::new(3, 0, text.into());
 
     let buf1_op = buffer1.edit([(1..2, "12")]);
     assert_eq!(buffer1.text(), "a12cdef");
@@ -659,7 +677,7 @@ fn test_random_concurrent_edits(mut rng: StdRng) {
     let mut network = Network::new(rng.clone());
 
     for i in 0..peers {
-        let mut buffer = Buffer::new(i as ReplicaId, 0, History::new(base_text.clone().into()));
+        let mut buffer = Buffer::new(i as ReplicaId, 0, base_text.clone().into());
         buffer.history.group_interval = Duration::from_millis(rng.gen_range(0..=200));
         buffers.push(buffer);
         replica_ids.push(i as u16);

crates/text/src/text.rs 🔗

@@ -63,6 +63,7 @@ pub struct BufferSnapshot {
     remote_id: u64,
     visible_text: Rope,
     deleted_text: Rope,
+    line_ending: LineEnding,
     undo_map: UndoMap,
     fragments: SumTree<Fragment>,
     insertions: SumTree<InsertionFragment>,
@@ -86,6 +87,12 @@ pub struct Transaction {
     pub ranges: Vec<Range<FullOffset>>,
 }
 
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum LineEnding {
+    Unix,
+    Windows,
+}
+
 impl HistoryEntry {
     pub fn transaction_id(&self) -> TransactionId {
         self.transaction.id
@@ -148,9 +155,9 @@ impl HistoryEntry {
 }
 
 #[derive(Clone)]
-pub struct History {
+struct History {
     // TODO: Turn this into a String or Rope, maybe.
-    pub base_text: Arc<str>,
+    base_text: Arc<str>,
     operations: HashMap<clock::Local, Operation>,
     undo_stack: Vec<HistoryEntry>,
     redo_stack: Vec<HistoryEntry>,
@@ -539,13 +546,18 @@ pub struct UndoOperation {
 }
 
 impl Buffer {
-    pub fn new(replica_id: u16, remote_id: u64, history: History) -> Buffer {
+    pub fn new(replica_id: u16, remote_id: u64, mut base_text: String) -> Buffer {
+        let line_ending = LineEnding::detect(&base_text);
+        LineEnding::strip_carriage_returns(&mut base_text);
+
+        let history = History::new(base_text.into());
         let mut fragments = SumTree::new();
         let mut insertions = SumTree::new();
 
         let mut local_clock = clock::Local::new(replica_id);
         let mut lamport_clock = clock::Lamport::new(replica_id);
         let mut version = clock::Global::new();
+
         let visible_text = Rope::from(history.base_text.as_ref());
         if visible_text.len() > 0 {
             let insertion_timestamp = InsertionTimestamp {
@@ -576,6 +588,7 @@ impl Buffer {
                 remote_id,
                 visible_text,
                 deleted_text: Rope::new(),
+                line_ending,
                 fragments,
                 insertions,
                 version,
@@ -658,7 +671,7 @@ impl Buffer {
         let mut new_insertions = Vec::new();
         let mut insertion_offset = 0;
 
-        let mut ranges = edits
+        let mut edits = edits
             .map(|(range, new_text)| (range.to_offset(&*self), new_text))
             .peekable();
 
@@ -666,12 +679,12 @@ impl Buffer {
             RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0));
         let mut old_fragments = self.fragments.cursor::<FragmentTextSummary>();
         let mut new_fragments =
-            old_fragments.slice(&ranges.peek().unwrap().0.start, Bias::Right, &None);
+            old_fragments.slice(&edits.peek().unwrap().0.start, Bias::Right, &None);
         new_ropes.push_tree(new_fragments.summary().text);
 
         let mut fragment_start = old_fragments.start().visible;
-        for (range, new_text) in ranges {
-            let new_text = new_text.into();
+        for (range, new_text) in edits {
+            let new_text = LineEnding::strip_carriage_returns_from_arc(new_text.into());
             let fragment_end = old_fragments.end(&None).visible;
 
             // If the current fragment ends before this range, then jump ahead to the first fragment
@@ -714,6 +727,7 @@ impl Buffer {
             // Insert the new text before any existing fragments within the range.
             if !new_text.is_empty() {
                 let new_start = new_fragments.summary().text.visible;
+
                 edits_patch.push(Edit {
                     old: fragment_start..fragment_start,
                     new: new_start..new_start + new_text.len(),
@@ -805,6 +819,10 @@ impl Buffer {
         edit_op
     }
 
+    pub fn set_line_ending(&mut self, line_ending: LineEnding) {
+        self.snapshot.line_ending = line_ending;
+    }
+
     pub fn apply_ops<I: IntoIterator<Item = Operation>>(&mut self, ops: I) -> Result<()> {
         let mut deferred_ops = Vec::new();
         for op in ops {
@@ -1412,6 +1430,8 @@ impl Buffer {
             fragment_summary.text.deleted,
             self.snapshot.deleted_text.len()
         );
+
+        assert!(!self.text().contains("\r\n"));
     }
 
     pub fn set_group_interval(&mut self, group_interval: Duration) {
@@ -1549,6 +1569,10 @@ impl BufferSnapshot {
         self.visible_text.to_string()
     }
 
+    pub fn line_ending(&self) -> LineEnding {
+        self.line_ending
+    }
+
     pub fn deleted_text(&self) -> String {
         self.deleted_text.to_string()
     }
@@ -2310,6 +2334,50 @@ impl operation_queue::Operation for Operation {
     }
 }
 
+impl Default for LineEnding {
+    fn default() -> Self {
+        #[cfg(unix)]
+        return Self::Unix;
+
+        #[cfg(not(unix))]
+        return Self::CRLF;
+    }
+}
+
+impl LineEnding {
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            LineEnding::Unix => "\n",
+            LineEnding::Windows => "\r\n",
+        }
+    }
+
+    pub fn detect(text: &str) -> Self {
+        if let Some(ix) = text[..cmp::min(text.len(), 1000)].find(&['\n']) {
+            let text = text.as_bytes();
+            if ix > 0 && text[ix - 1] == b'\r' {
+                Self::Windows
+            } else {
+                Self::Unix
+            }
+        } else {
+            Self::default()
+        }
+    }
+
+    pub fn strip_carriage_returns(text: &mut String) {
+        text.retain(|c| c != '\r')
+    }
+
+    fn strip_carriage_returns_from_arc(text: Arc<str>) -> Arc<str> {
+        if text.contains('\r') {
+            text.replace('\r', "").into()
+        } else {
+            text
+        }
+    }
+}
+
 pub trait ToOffset {
     fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize;
 }